sfali / grpc-rest-gateway   0.9.1

Apache License 2.0 GitHub

Rest gateway for GRPC end point

Scala versions: 3.x 2.13 2.12

gRPC to REST Gateway

Table of Contents

The gRPC-REST Gateway is a code generation tool and runtime library that automatically generates REST API endpoints from gRPC service definitions. It provides a reverse-proxy server that converts HTTP REST requests into gRPC calls, enabling dual-protocol access to backend services. This project is based on work done by grpcgateway.

1. About this project

1.1. 1. Code Generation Module (code-gen)

The code generation module processes protobuf definitions and generates corresponding gateway handlers and OpenAPI specifications.

1.1.1. Key Parts

  • Gateway Generator: Creates HTTP-to-gRPC handler classes

  • OpenAPI Generator: Generates OpenAPI 3.1.0 specifications

  • Path Parser: Parses HTTP patterns and builds routing trees

  • Template Engine: Generates Scala code for different runtime implementations

1.1.2. Code Generation Flow

+-------------+     +-----------------+     +----------------+     +------------------+
|             |     |                 |     |                |     |                  |
| Proto Files |---->| Protobuf Parser |---->| Path Parser    |---->| Code Templates   |
|             |     |                 |     |                |     |                  |
+-------------+     +-------+---------+     +-------+--------+     +--------+---------+
                            |                     |                         |
                            v                     v                         v
                   +----------------+    +----------------+         +------------------+
                   |                |    |                |         |                  |
                   | Service Model  |    | Routing Tree   |         | Generated Code   |
                   |                |    |                |         |                  |
                   +----------------+    +----------------+         +------------------+

1.2. 2. Runtime Libraries

The runtime libraries provide the HTTP-to-gRPC bridge functionality with multiple implementation options.

1.2.1. 2.1 Runtime Core (runtime-core)

Core abstractions and shared functionality:

  • GrpcGatewayHandler: Abstract base class for HTTP request dispatching

  • SwaggerHandler: OpenAPI specification serving and Swagger UI

  • Error Handling: gRPC to HTTP status code mapping

  • Request/Response Conversion: JSON to protobuf message conversion

1.2.2. 2.2 Runtime Implementations

Netty Implementation (runtime-netty)

  • Uses Netty HTTP server

  • Direct gRPC channel management

  • Synchronous request handling

Pekko Implementation (runtime-pekko)

  • Uses Apache Pekko HTTP

  • Asynchronous, actor-based processing

  • Route-based DSL for HTTP handling

Akka Implementation (runtime-akka)

  • Uses Akka HTTP

  • Similar to Pekko implementation

There are three main classes in each runtime implementation along with server classes to build REST server:

  1. GrpcGatewayHandler — base abstract class / trait responsible for dispatching given HTTP request to the corresponding function in gRPC service.

  2. SwaggerHandler — responsible for Open API yaml files and generate Swagger UI.

  3. GatewayServer — responsible for starting HTTP server at the given host and port.

1.3. 3. Server Components

1.3.1. 3.1 Gateway Server

Main server component that orchestrates the gateway functionality:

  • HTTP Server: Handles incoming REST requests

  • gRPC Client: Communicates with backend gRPC services

  • Configuration Management: Host, port, TLS settings

  • Lifecycle Management: Server startup and shutdown

1.3.2. 3.2 Swagger Handler

Provides API documentation and exploration:

  • OpenAPI Specification: Serves generated YAML/JSON specs

  • Swagger UI: Interactive API documentation

  • Service Discovery: Lists available gRPC services

2. System Architecture

2.1. High-Level Architecture

+----------------+      +---------------------+      +-------------------+
|                |      |                     |      |                   |
|  REST Client   |<---->|  gRPC-REST Gateway  |<---->|   gRPC Server     |
|                |      |                     |      |                   |
+----------------+      +--------+------------+      +--------+----------+
                                 |                            |
                                 |                            |
                        +--------v--------+          +--------v--------+
                        |                 |          |                 |
                        |   Swagger UI    |          |  Business Logic |
                        |                 |          |                 |
                        +-----------------+          +-----------------+

2.2. Component Architecture

+----------------------+     +----------------------+     +----------------------+
|                      |     |                      |     |                      |
|   Code Generation    |     |   Runtime Libraries  |     |   Server Components  |
|                      |     |                      |     |                      |
| +------------------+ |     | +------------------+ |     | +------------------+ |
| | Gateway Generator| |     | | Runtime Core     | |     | | Gateway Server   | |
| +------------------+ |     | +------------------+ |     | +------------------+ |
| | OpenAPI Generator| |     | | Runtime Netty    | |     | | Swagger Handler  | |
| +------------------+ |     | +------------------+ |     | +------------------+ |
| | Annotations      | |     | | Runtime Pekko    | |     | | HTTP Handlers    | |
| +------------------+ |     | +------------------+ |     | +------------------+ |
|                      |     | | Runtime Akka     | |     |                      |
|                      |     | +------------------+ |     |                      |
+----------------------+     +----------------------+     +----------------------+

3. Data Flow

3.1. Request Processing Flow

+-------------+     +----------------+     +-----------------+     +----------------+     +---------------+
|             |     |                |     |                 |     |                |     |               |
| HTTP Request|---->| Route Matching |---->| Request Parsing |---->| gRPC Call      |---->| HTTP Response |
|             |     |                |     |                 |     |                |     |               |
+-------------+     +-------+--------+     +-------+---------+     +-------+--------+     +-------+-------+
                            |                       |                       |                       |
                            v                       v                       v                       v
                   +----------------+    +----------------+    +----------------+           +----------------+
                   |                |    |                |    |                |           |                |
                   | Path/Method    |    | JSON to Proto  |    | gRPC Service   |           | Proto to JSON  |
                   | Validation     |    | Conversion     |    | Invocation     |           | Conversion     |
                   +----------------+    +----------------+    +----------------+           +----------------+

3.2. Error Handling Flow

+----------------+     +----------------+     +----------------+     +----------------+
|                |     |                |     |                |     |                |
| gRPC Error     |---->| Status Mapping |---->| HTTP Status    |---->| Error Response |
| Exception      |     | Table          |     | Code           |     | Body           |
|                |     |                |     |                |     |                |
+----------------+     +-------+--------+     +-------+--------+     +-------+--------+
                               |                       |                     |
                               v                       v                     v
                      +----------------+        +----------------+    +----------------+
                      |                |        |                |    |                |
                      | gRPC Status    |        | HTTP Status    |    | JSON Error     |
                      | to HTTP        |        | Code (400-500) |    | Response       |
                      | Mapping        |        |                |    |                |
                      +----------------+        +----------------+    +----------------+

4. Adding gRPC-Rest-Gateway annotation to a protobuf file

Each RPC must define the HTTP method and path using the google.api.http annotation, which can be done by importing google/api/annotations.proto.

So for the following RPC:

rpc GetRequest (rest_gateway_test.api.model.TestRequestB) returns (rest_gateway_test.api.model.TestResponseB)

can be mapped to HTTP GET /restgateway/test/testserviceb by adding the following annotation:

rpc GetRequest (rest_gateway_test.api.model.TestRequestB) returns (rest_gateway_test.api.model.TestResponseB) {
    option (google.api.http) = {
      get: "/restgateway/test/testserviceb"
    };
}

The detail mapping between RPC and HTTP method can be found here.

4.1. Extension to the standard Google http annotations to configure HTTP status codes

Standard Google http annotations don’t provide a way to configure HTTP status code, default HTTP status code is mapped to 200 OK. The gRPC-rest gateway provides its own annotation to configure HTTP status code other than 200 OK.

Add the following dependency to your build.sbt:

  "io.github.sfali23" %% "grpc-rest-gateway-annotations" % "0.9.1" % "compile,protobuf"

The following is definition of annotation:

syntax = "proto3";

import "scalapb/scalapb.proto";
import "google/protobuf/descriptor.proto";

package grpc_rest_gateway.api;

option java_multiple_files = false;
option java_package = "com.improving.grpc_rest_gateway.api";
option java_outer_classname = "GrpcRestGatewayProto";

option (scalapb.options) = {
  flat_package: true
  single_file: true
  retain_source_code_info: true
  preserve_unknown_fields: false
  package_name: "com.improving.grpc_rest_gateway.api"
};

message StatusDescription {
  int32 status = 1; // Valid HTTP status code
  string description = 2; // optional, description of given status
}

message Statuses {
  StatusDescription successStatus = 1; // Default success status, default value is '200'
  repeated StatusDescription otherStatus = 2; // Other status and their descriptions, this will be used in OpenApi documentation
}

extend google.protobuf.MethodOptions {
  Statuses statuses = 50000;
}

The following definition will configure default status to 204 NoContent.

option (grpc_rest_gateway.api.statuses) = {
      successStatus: {
        status: 204 // returns default status of No Content (204)
        description: "Update resource"
      },
      // documentation of other statuses
      otherStatus: [
        {
          status: 400
          description: "Bad request"
        },
        {
          status: 404
          description: "Not found"
        }
      ]
};

5. Set up your project

5.1. Configure compiler plugin for code generation

The code-generator plugin has following options:

  • scala3Sources — Generate Scala 3 sources — It is used for using * as wild card import instead of _.

    • For Scala 2.12 or later, set to true if -Xsource:3 scala compiler option is used.

    • Always enabled if useScala3Features flag is on.

  • useScala3Features — Whether to use Scala 3 features. It is used to use using and given instead of implicits.

  • implementationType — The target implementation type (Netty, Pekko, Akka)

The OpenApi specs generator has following options:

  • version — the version of the OpenApi specification to generate, default value is 0.1.0-SNAPSHOT.

See OpenAPI Specifications Configuration for more details.

To generate Scala classes for gateway handler and OpenApi specification, add following in plugin.sbt:

addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.8")

libraryDependencies ++= Seq(
  "com.thesamet.scalapb" %% "compilerplugin" % "0.11.8",
  "io.github.sfali23" %% "grpc-rest-gateway-code-gen" % "0.9.1"
)

And following in the build.sbt:

Compile / PB.targets := Seq(
  scalapb.gen()  (Compile / sourceManaged).value / "scalapb",
  grpc_rest_gateway.gatewayGen(implementationType = grpc_rest_gateway.ImplementationType.Netty)  (Compile / sourceManaged).value / "scalapb",
  grpc_rest_gateway.openApiGen()  (Compile / resourceManaged).value / "specs"
)

// change the value of the "implementationType" parameter to grpc_rest_gateway.ImplementationType.Pekko or
// grpc_rest_gateway.ImplementationType.Akka to generate Pekko or Akka based implementation

// generate code for Scala 3
Compile / PB.targets := Seq(
  scalapb.gen(scala3Sources = true)  (Compile / sourceManaged).value / "scalapb",
  grpc_rest_gateway.gatewayGen(useScala3Features = true, implementationType = grpc_rest_gateway.ImplementationType.Pekko)  (Compile / sourceManaged).value / "scalapb",
  grpc_rest_gateway.openApiGen()  (Compile / resourceManaged).value / "specs"
)

// Add the following to add openapi specs into the classpath
Compile / resourceGenerators += (Compile / PB.generate)
      .map(.filter(.getName.endsWith("yml")))
      .taskValue

5.2. Configure dependencies for Netty-based implementation

Add the following dependencies

val AppVersion = "0.9.1"
val ScalaPb: String = scalapb.compiler.Version.scalapbVersion
val GrpcJava: String = scalapb.compiler.Version.grpcJavaVersion
val ScalaPbJson = "0.12.1"
libraryDependencies ++= Seq(
  "io.github.sfali23" %% "grpc-rest-gateway-runtime-netty" % AppVersion,
  "com.thesamet.scalapb" %% "compilerplugin" % ScalaPb % "compile;protobuf",
  "com.thesamet.scalapb" %% "scalapb-runtime" % ScalaPb % "compile;protobuf",
  "com.thesamet.scalapb" %% "scalapb-runtime-grpc" % ScalaPb,
  "io.grpc" % "grpc-netty" % GrpcJava,
  "com.thesamet.scalapb" %% "scalapb-json4s" % ScalaPbJson,
  "com.thesamet.scalapb.common-protos" %% "proto-google-common-protos-scalapb_0.11" % "2.9.6-0" % "compile,protobuf",
  // optional if using custom annotation
  "io.github.sfali23" %% "grpc-rest-gateway-annotations" % AppVersion,
  "io.github.sfali23" %% "grpc-rest-gateway-annotations" % AppVersion % "protobuf"
)

5.3. Configure dependencies for Pekko based implementation

Add the following dependencies:

val AppVersion = "0.9.1"
val ScalaPb: String = scalapb.compiler.Version.scalapbVersion
val GrpcJava: String = scalapb.compiler.Version.grpcJavaVersion

lazy val root = project
  .in(file("."))
  .enablePlugins(PekkoGrpcPlugin)
  .settings(
      libraryDependencies = Seq(
        "io.github.sfali23" %% "grpc-rest-gateway-runtime-pekko" % AppVersion,
        "org.apache.pekko" %% "pekko-actor" % "1.1.2",
        "org.apache.pekko" %% "pekko-actor-typed" % "1.1.2",
        "org.apache.pekko" %% "pekko-stream-typed" % "1.1.2",
        "org.apache.pekko" %% "pekko-http" % "1.1.0",
        "org.apache.pekko" %% "pekko-grpc-runtime" % "1.1.1",
        "com.thesamet.scalapb.common-protos" %% "proto-google-common-protos-scalapb_0.11" % "2.9.6-0" % "compile,protobuf",
        // optional if using custom annotation
        "io.github.sfali23" %% "grpc-rest-gateway-annotations" % AppVersion,
        "io.github.sfali23" %% "grpc-rest-gateway-annotations" % AppVersion % "protobuf"
      ),
      pekkoGrpcGeneratedSources := generatedSource,
      pekkoGrpcCodeGeneratorSettings := Seq("grpc", "single_line_to_proto_string"),
      Compile / PB.targets = Seq(
        grpc_rest_gateway
          .gatewayGen(
              scala3Sources = true,
              implementationType = grpc_rest_gateway.ImplementationType.Pekko
           )  crossTarget.value / "pekko-grpc" / "main",
          grpc_rest_gateway.openApiGen()  (Compile / resourceManaged).value / "specs"
      ),
      Compile / resourceGenerators += (Compile / PB.generate)
      .map(.filter(.getName.endsWith("yml")))
      .taskValue
  )

5.4. Configure dependencies for Akka based implementation

Should be similar to Pekko by replacing corresponding Akka dependencies.

6. Code generation library — grpc-rest-gateway-code-gen

Code generation library is responsible for reading given Protobuf files and generating corresponding implementation of GrpcGatewayHandler based on its runtime library. The runtime handler can be generated by passing implementationType parameter:

There are three different plugins to generate runtime handlers, namely:

  1. grpc_rest_gateway.gatewayGen(implementationType = grpc_rest_gateway.ImplementationType.Netty) for Netty based implementation

  2. grpc_rest_gateway.gatewayGen(implementationType = grpc_rest_gateway.ImplementationType.Pekko) for Pekko based implementation

  3. grpc_rest_gateway.gatewayGen(implementationType = grpc_rest_gateway.ImplementationType.Akka) for Akka based implementation

Warning
Akka implementation hasn’t been tested yet due version dependency eviction in e2e testing module.

For example, the following Protobuf definition:

syntax = "proto3";

package rest_gateway_test.api;

import "scalapb/scalapb.proto";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
import "common.proto";
import "grpc_rest_gateway/api/annotations.proto";

option java_multiple_files = false;
option java_package = "rest_gateway_test.api.java_api";
option java_outer_classname = "TestServiceBProto";
option objc_class_prefix = "TS2P";

option (scalapb.options) = {
  single_file: true
  lenses: true
  retain_source_code_info: true
  preserve_unknown_fields: false
  flat_package: true
  package_name: "rest_gateway_test.api.scala_api"
};

option (grpc_rest_gateway.api.openapi_info) = {
   version: "1.0.0"
};

// Test service B
service TestServiceB {
  rpc GetRequest (rest_gateway_test.api.model.TestRequestB) returns (rest_gateway_test.api.model.TestResponseB) {
    option (google.api.http) = {
      get: "/restgateway/test/testserviceb"
    };
  }

  rpc Process (rest_gateway_test.api.model.TestRequestB) returns (rest_gateway_test.api.model.TestResponseB) {
    option (google.api.http) = {
      post: "/restgateway/test/testserviceb"
      body: "*"
    };
  }

  rpc Update (rest_gateway_test.api.model.TestRequestB) returns (google.protobuf.Empty) {
    option (google.api.http) = {
      put: "/restgateway/test/testserviceb/update"
      body: "*"
    };

    option (grpc_rest_gateway.api.statuses) = {
      successStatus: {
        status: 204 // returns default status of No Content (204)
        description: "Update resource"
      },
      // documentation of other statuses
      otherStatus: [
        {
          status: 400
          description: "Bad request"
        },
        {
          status: 404
          description: "Not found"
        }
      ]
    };
  }
}

6.1. Generated Open API specification:

openapi: 3.1.0
info:
  version: 1.0.0
  description: "REST API generated from TestServiceB.proto"
  title: "TestServiceB.proto"
tags:
  - name: TestServiceB
    description: Test service B
paths:
  /restgateway/test/testserviceb:
    get:
      tags:
        - GetRequest
      description: Generated from GetRequest
      parameters:
        - name: requestId
          in: query
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TestResponseB"
        default:
          description: Unexpected error
    post:
      tags:
        - Process
      description: Generated from Process
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TestRequestB"
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TestResponseB"
        default:
          description: Unexpected error
  /restgateway/test/testserviceb/update:
    put:
      tags:
        - Update
      description: Generated from Update
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TestRequestB"
      responses:
        "204":
          description: Update resource
        "400":
          description: Bad request
        "404":
          description: Not found
        default:
          description: Unexpected error
components:
  schemas:
    TestRequestB:
      type: object
      properties:
        requestId:
          type: integer
          format: int64
          description: requestId
    TestResponseB:
      type: object
      properties:
        success:
          type: boolean
        request_id:
          type: integer
          format: int64
          description: request_id
        result:
          type: string
          description: result

6.2. Generated Netty based GrpcGatewayHandler

/*
 * Generated by GRPC-REST gateway compiler. DO NOT EDIT.
 */
package rest_gateway_test.api.scala_api

import scalapb.GeneratedMessage
import io.grpc.ManagedChannel
import io.netty.handler.codec.http.{HttpMethod, QueryStringDecoder}

import com.improving.grpc_rest_gateway.runtime
import runtime.core.*
import runtime.handlers.*

import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try

object TestServiceBGatewayHandler {
  private val GetGetRequestPath = "/restgateway/test/testserviceb"
  private val PostProcessPath = "/restgateway/test/testserviceb"
  private val PutUpdatePath = "/restgateway/test/testserviceb/update"

  def apply(channel: ManagedChannel)(implicit ec: ExecutionContext): TestServiceBGatewayHandler =
    new TestServiceBGatewayHandler(channel)
}

class TestServiceBGatewayHandler(channel: ManagedChannel)(implicit ec: ExecutionContext)
  extends GrpcGatewayHandler(channel)(ec) {
  import TestServiceBGatewayHandler.*
  override val serviceName: String = "TestServiceB"
  override val specificationName: String = "TestServiceB"
  private lazy val client = TestServiceBGrpc.stub(channel)
  override protected val httpMethodsToUrisMap: Map[String, Seq[String]] = Map(
    "GET" -> Seq(
      GetGetRequestPath
    ),
    "POST" -> Seq(
      PostProcessPath
    ),
    "PUT" -> Seq(
      PutUpdatePath
    )
  )

  override protected def dispatchCall(method: HttpMethod, uri: String, body: String): Future[(Int, GeneratedMessage)] = {
    val queryString = new QueryStringDecoder(uri)
    val path = queryString.path
    val methodName = method.name
    if (isSupportedCall(HttpMethod.GET.name, GetGetRequestPath, methodName, path))
      dispatchGetRequest(200, mergeParameters(GetGetRequestPath, queryString))
    else if (isSupportedCall(HttpMethod.POST.name, PostProcessPath, methodName, path))
      dispatchProcess(200, body)
    else if (isSupportedCall(HttpMethod.PUT.name, PutUpdatePath, methodName, path))
      dispatchUpdate(204, body)
    else Future.failed(GatewayException.toInvalidArgument(s"No route defined for $methodName($path)"))
  }

  private def dispatchGetRequest(statusCode: Int, parameters: Map[String, Seq[String]]) = {
    val input = Try {
      val requestId = parameters.toLongValue("requestId")
      rest_gateway_test.api.model.TestRequestB(requestId = requestId)
    }
    toResponse(input, client.getRequest, statusCode)
  }

  private def dispatchProcess(statusCode: Int, body: String) = {
    val input = parseBody[rest_gateway_test.api.model.TestRequestB](body)
    toResponse(input, client.process, statusCode)
  }

  private def dispatchUpdate(statusCode: Int, body: String) = {
    val input = parseBody[rest_gateway_test.api.model.TestRequestB](body)
    toResponse(input, client.update, statusCode)
  }
}

6.3. Generated Pekko based GrpcGatewayHandler

/*
 * Generated by GRPC-REST gateway compiler. DO NOT EDIT.
 */
package rest_gateway_test.api.scala_api

import com.improving.grpc_rest_gateway.runtime
import runtime.core._
import runtime.handlers.GrpcGatewayHandler

import org.apache.pekko
import pekko.grpc.GrpcClientSettings
import pekko.actor.ClassicActorSystemProvider
import pekko.http.scaladsl.server.Route
import pekko.http.scaladsl.server.Directives._

import scala.concurrent.ExecutionContext
import scala.util.Try

class TestServiceBGatewayHandler(settings: GrpcClientSettings)(implicit sys: ClassicActorSystemProvider) extends GrpcGatewayHandler {

  private implicit val ec: ExecutionContext = sys.classicSystem.dispatcher
  private lazy val client = TestServiceBClient(settings)
  override val specificationName: String = "TestServiceB"

  override val route: Route = handleExceptions(exceptionHandler) {
      pathPrefix("restgateway") {
        pathPrefix("test") {
          pathPrefix("testserviceb") {
            concat(
              pathEnd {
                concat(
                  get {
                    parameterMultiMap { queryParameters =>
                      dispatchGetRequest(200, queryParameters)
                    }
                  },
                  post {
                    entity(as[String]) { body =>
                      dispatchProcess(200, body)
                    }
                  }
                )
              },
              pathPrefix("update") {
                pathEnd {
                  put {
                    entity(as[String]) { body =>
                      dispatchUpdate(204, body)
                    }
                  }
                }
              }
            )
          }
        }
      }
    }

  private def dispatchGetRequest(statusCode: Int, parameters: Map[String, Seq[String]]) = {
    val input = Try {
      val requestId = parameters.toLongValue("requestId")
      rest_gateway_test.api.model.TestRequestB(requestId = requestId)
    }
    completeResponse(input, client.getRequest, statusCode)
  }

  private def dispatchProcess(statusCode: Int, body: String) = {
    val input = parseBody[rest_gateway_test.api.model.TestRequestB](body)
    completeResponse(input, client.process, statusCode)
  }

  private def dispatchUpdate(statusCode: Int, body: String) = {
    val input = parseBody[rest_gateway_test.api.model.TestRequestB](body)
    completeResponse(input, client.update, statusCode)
  }
}

object TestServiceBGatewayHandler {

  def apply(settings: GrpcClientSettings)(implicit sys: ClassicActorSystemProvider): GrpcGatewayHandler = {
    new TestServiceBGatewayHandler(settings)
  }

  def apply(clientName: String)(implicit sys: ClassicActorSystemProvider): GrpcGatewayHandler = {
    TestServiceBGatewayHandler(GrpcClientSettings.fromConfig(clientName))
  }
}

6.4. Generated Akka based GrpcGatewayHandler

Should be similar to Pekko with pekko in import statement will be replaced by akka.

7. Setting HTTP gateway server

Implement your gRPC services as per your need and run gRPC server. Gateway server can be build and run as follows:

7.1. Netty-based Gateway server

import com.improving.grpc_rest_gateway.runtime.server.GatewayServer
import rest_gateway_test.api.scala_api.TestServiceB.TestServiceBGatewayHandler
import scala.concurrent.ExecutionContext

implicit val ex: ExecutionContext = ??? // provide ExecutionContext
val server = GatewayServer(
      serviceHost = "localhost",
      servicePort = 8080, // assuming gRPC server is running on port 8080
      gatewayPort = 7070, // REST end point is running at port 7070
      toHandlers = channel => Seq(TestServiceBGatewayHandler(channel)),
      executor = None, // Executor is useful if you want to allocate different thread pool for REST endpoint
      usePlainText = true
    )
server.start()

// stop server once done
server.stop()

// via Typesafe config
val mainConfig = ConfigFactory.load()
val server = GatewayServer(
  config = mainConfig.getConfig("rest-gateway"),
  toHandlers = channel => Seq(TestServiceBGatewayHandler(channel)),
  executor = None
)

Alternatively serviceHost, servicePort, gatewayPort, usePlainText can be overriden via environment variables GRPC_HOST, GRPC_SERVICE_PORT, REST_GATEWAY_PORT, and GRPC_USE_PLAIN_TEXT respectively.

// rest-gateway config is defined as follows:

rest-gateway {
  host = "0.0.0.0"
  host = ${?GRPC_HOST}
  service-port = 8080
  service-port = ${?GRPC_SERVICE_PORT}
  gateway-port = 7070
  gateway-port = ${?REST_GATEWAY_PORT}
  use-plain-text = "true"
  use-plain-text = ${?GRPC_USE_PLAIN_TEXT}
}

7.2. Pekko based Gateway server

Providing Pekko gRPC client configuration is defined as follows:

pekko {
  grpc {
    client {
      pekko-gateway {
        host = "0.0.0.0" // gRPC host
        port = 8080 // grPC port
        use-tls = false
      }
    }
  }
}

// rest gateway config
rest-gateway {
  host = "0.0.0.0"
  host = ${?REST_GATEWAY_HOST}
  port = 7070
  port = ${?REST_GATEWAY_PORT}
  hard-termination-deadline = 10.seconds // For Coordinated shutdown
  hard-termination-deadline = ${?REST_GATEWAY_HARD_TERMINATION_DEADLINE}
}

Gateway server can be initialized as follows:

implicit val system: ActorSystem[?] = ActorSystem[Nothing](Behaviors.empty, "grpc-rest-gateway-pekko")

val settings = GrpcClientSettings.fromConfig("pekko-gateway")
val config = system.settings.config
val restGatewayConfig = config.getConfig("rest-gateway")
GatewayServer(
  restGatewayConfig,
  TestServiceBGatewayHandler(settings)
).run()

// Or using HttSettings

GatewayServer(
  HttpSettings(restGatewayConfig),
  TestServiceBGatewayHandler(settings)
).run()

7.3. Akka based Gateway server

Providing Akka gRPC client configuration is defined as follows:

akka {
  grpc {
    client {
      pekko-gateway {
        host = "0.0.0.0" // gRPC host
        port = 8080 // grPC port
        use-tls = false
      }
    }
  }
}

// rest gateway config
rest-gateway {
  host = "0.0.0.0"
  port = 7070 // Gateway port
}

Gateway server can be initialized as follows:

implicit val system: ActorSystem[?] = ActorSystem[Nothing](Behaviors.empty, "grpc-rest-gateway-pekko")

val settings = GrpcClientSettings.fromConfig("pekko-gateway")
val config = system.settings.config
GatewayServer(
  config.getConfig("rest-gateway"),
  TestServiceBGatewayHandler(settings)
).run()

8. Error handling and HTTP status code mapping

gRPC-REST gateway has built in mapping between gRPC and HTTP status codes. Following is the mappings between two systems:

gRPC status code HTTP status code

OK

OK (200)

DATA_LOSS

Partial Content (206)

INVALID_ARGUMENT, OUT_OF_RANGE

Bad Request (400)

UNAUTHENTICATED

Unauthorized(401)

PERMISSION_DENIED

Forbidden (403)

NOT_FOUND, UNKNOWN

Not Found (404)

UNAVAILABLE

Not Acceptable (406)

ALREADY_EXISTS

Conflict (409)

ABORTED, CANCELLED

Gone (410)

FAILED_PRECONDITION

Precondition Failed (412)

INTERNAL

Internal Server Error (500)

UNIMPLEMENTED

Not Implemented (501)

DEADLINE_EXCEEDED

Gateway Timeout (504)

RESOURCE_EXHAUSTED

Insufficient Storage (507)

Note: Any unmapped code will be mapped to Internal Server Error (500).

Build io.grpc.StatusRuntimeException using io.grpc.protobuf.StatusProto to set corresponding status code and message in your implementation of gRPC server.

import com.google.rpc.{Code, Status}
import io.grpc.protobuf.StatusProto
import scala.concurrent.Future

// handle bad request
Future.failed(StatusProto.toStatusRuntimeException(
        Status
          .newBuilder()
          .setCode(Code.INVALID_ARGUMENT_VALUE)
          .setMessage("Invalid argument")
          .build())
)

// not found
Future.failed(StatusProto.toStatusRuntimeException(
        Status
          .newBuilder()
          .setCode(Code.NOT_FOUND_VALUE)
          .setMessage("Not found")
          .build())
)

9. Protobuf to REST mapping

Following is how Protobuf to REST mapping will work as described in the documentation.

Given following Protobuf definition:

 service Messaging {
       rpc GetMessage(GetMessageRequest) returns (Message) {
         option (google.api.http) = {
           get: "/v1/messages/{message_id}/{sub.subfield}"
           additional_bindings {
              get: "/v1/messages/{message_id}"
           }
         };
       }

       rpc PostMessage(GetMessageRequest) returns (Message) {
         option (google.api.http) = {
           put: "/v1/messages/{message_id}"
           body: "sub"
         };
       }

       rpc PostMessage(GetMessageRequest) returns (Message) {
         option (google.api.http) = {
           post: "/v1/messages"
           body: "*"
         };
       }
}

message GetMessageRequest {
  message SubMessage {
    string subfield = 1;
  }
  string message_id = 1;
  SubMessage sub = 2;
}

message Message {
  string text = 1;
}

Following mapping defines how HTTP request supposed to be constructed.

HTTP method: GET
Path: /v1/messages/{message_id}/{sub.subfield}
HTTP request: http://localhost:7070/v1/messages/xyz/abc
Mapping: Both message_id and sub.subfield are mapped as path variables

HTTP method: GET
Path: /v1/messages/{message_id}
HTTP request: http://localhost:7070/v1/messages/xyz?sub.subfield=abc
Mapping: message_id is mapped as path variable while sub.subfield is mapped as query parameter

HTTP method: PUT
Path: |http://localhost:7070/v1/messages/xyz
HTTP request: http://localhost:7070/v1/messages/xyz?sub.subfield=abc [body: {"subfield": "sub"}]
Mapping: message_id is mapped as path variable while sub is mapped as body payload

HTTP method: POST
Path: /v1/messages
HTTP request: http://localhost:7070/v1/messages
Mapping: entire message is mapped as body payload

10. Run tests and sample app

e2e module contains test code and a sample app.

Tests can be run as follows:

sbt "nettyJVM212Test"
sbt "nettyJVM213Test"
sbt "nettyJVM3Test"
sbt "pekkoJVM212Test"
sbt "pekkoJVM213Test"
sbt "pekkoJVM3Test"

Sample app can be run as follows:

# For Scala 2.12
sbt "nettyJVM212Run"
sbt "pekkoJVM212Run"

# # For Scala 2.13
sbt "nettyJVM213Run"
sbt "pekkoJVM213Run"

# For Scala 3
sbt "nettyJVM3Run"
sbt "pekkoJVM3Run"

Open browser and paste following URL in address bar http://localhost:7070, you should see Open API specification for service.

swagger

11. Reference implementation using Apache Pekko

A reference implementation of Swagger petstore is attempted here. Follow steps described in README file to run reference implementation.

Open browser and paste following URL in address bar http://localhost:7070, you should see Open API specification for petstore service.

petstore

Following is corresponding proto file.

12. OpenAPI Specifications Configuration

The gRPC-REST Gateway automatically generates OpenAPI 3.1.0 specifications from your protobuf definitions and serves them through the Swagger UI. This section explains how to configure the OpenAPI specs generator and customize the specs directory.

12.1. OpenAPI Generator Options

The OpenAPI specs generator accepts the following configuration options:

  • version — The version of the OpenAPI specification to generate. Default value is 0.1.0-SNAPSHOT. This can be declared by setting the version option in the proto file.

    option (grpc_rest_gateway.api.openapi_info) = {
      version: "1.0.0"
    };
  • specsDirectory — The directory where OpenAPI specification files will be generated. This is configured at build time and runtime. The default value is specs.

12.2. Build-Time Configuration

12.2.1. Generating OpenAPI Specifications

To generate OpenAPI specifications during the build process, add the openApiGen target to your build.sbt:

Compile / PB.targets := Seq(
  scalapb.gen()  (Compile / sourceManaged).value / "scalapb",
  grpc_rest_gateway.gatewayGen(implementationType = grpc_rest_gateway.ImplementationType.Netty)  (Compile / sourceManaged).value / "scalapb",
  grpc_rest_gateway.openApiGen()  (Compile / resourceManaged).value / "specs"
)

// Add the following to include OpenAPI specs in the classpath
Compile / resourceGenerators += (Compile / PB.generate)
  .map(.filter(.getName.endsWith("yml")))
  .taskValue

In this configuration:

  • OpenAPI specs are generated to (Compile / resourceManaged).value / "specs" directory

  • The version parameter sets the OpenAPI specification version (e.g., "1.0.0")

  • The resourceGenerators task ensures the generated YAML files are included in the classpath

12.2.2. Using a Custom Specs Directory at Build Time

You can customize the output directory for OpenAPI specifications:

// Generate specs to a custom directory
Compile / PB.targets := Seq(
  scalapb.gen()  (Compile / sourceManaged).value / "scalapb",
  grpc_rest_gateway.gatewayGen(implementationType = grpc_rest_gateway.ImplementationType.Pekko)  (Compile / sourceManaged).value / "scalapb",
  grpc_rest_gateway.openApiGen()  (Compile / resourceManaged).value / "openapi-specs"
)

// Include the custom directory specs in the classpath
Compile / resourceGenerators += (Compile / PB.generate)
  .map(.filter(.getName.endsWith("yml")))
  .taskValue

12.3. Runtime Configuration

12.3.1. Default Configuration

The runtime configuration for OpenAPI specifications is managed through Typesafe Config (HOCON format). The default configuration is defined in reference.conf:

openapi {
  enabled = true
  enabled = ${?OPENAPI_ENABLED}
  specs-dir = "specs"
  specs-dir = ${?OPENAPI_SPECS_FOLDER}
}

Configuration properties:

  • enabled — Enable or disable OpenAPI/Swagger UI functionality. Default: true

  • specs-dir — The directory name where OpenAPI specification files are located in the classpath. Default: "specs"

Both properties can be overridden using environment variables:

  • OPENAPI_ENABLED — Set to true or false

  • OPENAPI_SPECS_FOLDER — Set to your custom directory name

12.3.2. Configuring a Custom Specs Directory

To use a custom specs directory, create or modify your application.conf:

openapi {
  enabled = true
  specs-dir = "my-custom-specs"
  // or empty string to look for specs in the root of the classpath
  // specs-dir = ""
}

Or override via environment variable:

export OPENAPI_SPECS_FOLDER="my-custom-specs"
Important
The specs-dir value must match the directory name where you generated the OpenAPI specs at build time (relative to the resource root).

12.3.3. Disabling OpenAPI/Swagger UI

To disable OpenAPI specifications and Swagger UI:

openapi {
  enabled = false
}

Or via environment variable:

export OPENAPI_ENABLED=false

When disabled, the gateway will not serve Swagger UI or OpenAPI specifications, reducing memory footprint and startup time.

12.4. Accessing OpenAPI Specifications

Once configured, you can access the OpenAPI specifications through the following endpoints:

12.5. Complete Configuration Example

12.5.1. Build Configuration (build.sbt)

lazy val myService = project
  .in(file("."))
  .settings(
    Compile / PB.targets := Seq(
      scalapb.gen()  (Compile / sourceManaged).value / "scalapb",
      grpc_rest_gateway.gatewayGen(
        implementationType = grpc_rest_gateway.ImplementationType.Pekko
      )  (Compile / sourceManaged).value / "scalapb",
      grpc_rest_gateway.openApiGen()  (Compile / resourceManaged).value / "api-specs"
    ),
    Compile / resourceGenerators += (Compile / PB.generate)
      .map(.filter(.getName.endsWith("yml")))
      .taskValue
  )

12.5.2. Runtime Configuration (application.conf)

# gRPC service configuration
pekko {
  grpc {
    client {
      my-service {
        host = "localhost"
        port = 8080
        use-tls = false
      }
    }
  }
}

# REST gateway configuration
rest-gateway {
  host = "0.0.0.0"
  host = ${?REST_GATEWAY_HOST}
  port = 7070
  port = ${?REST_GATEWAY_PORT}
  hard-termination-deadline = 10.seconds
  hard-termination-deadline = ${?REST_GATEWAY_HARD_TERMINATION_DEADLINE}
}

# OpenAPI configuration
openapi {
  enabled = true
  enabled = ${?OPENAPI_ENABLED}
  specs-dir = "api-specs"
  specs-dir = ${?OPENAPI_SPECS_FOLDER}
}

12.6. Troubleshooting

12.6.1. Swagger UI Not Loading

If Swagger UI is not loading, verify:

  1. OpenAPI is enabled: Check that openapi.enabled = true in your configuration

  2. Specs directory matches: Ensure the specs-dir in application.conf matches the directory used in build.sbt

  3. Specs are in classpath: Verify that resourceGenerators task is configured correctly

  4. YAML files exist: Check that .yml files were generated during compilation

12.6.2. Environment Variable Override Not Working

Ensure environment variables are set before starting the application:

# Set environment variables
export OPENAPI_ENABLED=true
export OPENAPI_SPECS_FOLDER="my-specs"

# Then run your application
sbt run

12.7. Troubleshooting YAML Generation

12.7.1. YAML Files Not Generated

If YAML files are not being generated:

  1. Check build configuration: Ensure openApiGen() is included in PB.targets

    Compile / PB.targets := Seq(
      scalapb.gen() -> (Compile / sourceManaged).value / "scalapb",
      grpc_rest_gateway.gatewayGen(...) -> (Compile / sourceManaged).value / "scalapb",
      grpc_rest_gateway.openApiGen() -> (Compile / resourceManaged).value / "specs"
    )
  2. Verify proto files have HTTP annotations: Only services with google.api.http annotations generate YAML

    rpc GetRequest (TestRequest) returns (TestResponse) {
      option (google.api.http) = {
        get: "/api/resource"
      };
    }
  3. Check compilation: Run sbt compile and look for protobuf generation messages

12.7.2. YAML Files Not in Classpath

If YAML files are generated but not accessible at runtime:

  1. Verify resourceGenerators: Ensure YAML files are included in classpath

    Compile / resourceGenerators += (Compile / PB.generate)
      .map(_.filter(_.getName.endsWith("yml")))
      .taskValue
  2. Check resource directory: YAML files should be in target/…​/classes/specs/

12.7.3. Version Not Applied from Proto File

If the version in generated YAML doesn’t match your proto file’s openapi_info:

  1. Verify import: Ensure you import the annotations proto file

    import "grpc_rest_gateway/api/annotations.proto";
  2. Check option syntax: Verify the openapi_info option is correctly formatted

    option (grpc_rest_gateway.api.openapi_info) = {
      version: "1.0.0"
    };
  3. Rebuild: Clean and recompile to ensure changes are picked up

    sbt clean compile

13. Limitations

  1. Streaming calls are not supported.

  2. HTTP headers are not currently supported