Running using the AWS serverless stack

Tapir server endpoints can be packaged and deployed as an AWS Lambda function. You can utilize a single lambda function for multiple endpoints (“Fat Lambda”), or deploy the same jar multiple times, so that each handles its own endpoint or subset of endpoints. To invoke the function, HTTP requests can be proxied through AWS API Gateway.

To configure API Gateway routes, and the Lambda function, tools like AWS SAM, AWS CDK or Terraform can be used, to automate cloud deployments.

For an overview of how this works in more detail, see this blog post.

Runtimes & interpreters

AWS Lambda supports several runtimes — the language-specific environment in which your function code executes. Tapir supports three of them, each described in its own section below.

Note: the handler type parameter (AwsRequest or AwsRequestV1) determines the expected API Gateway request format. Use AwsRequest for API Gateway V2 (HTTP API) and AwsRequestV1 for API Gateway V1 (REST API). V1 requests are automatically normalized to V2 internally.

Java runtime

With the Java runtime, you package your code as a fat JAR (via assembly) and upload it to AWS, which manages the JVM. The Lambda entry point implements the AWS RequestStreamHandler interface, which is provided by the aws-lambda-java-runtime-interface-client dependency (pulled in transitively by all tapir AWS Lambda modules). You can use this with direct-style, cats-effect, or ZIO:

Direct-style

No effect library needed. Extend SyncLambdaHandler, provide your endpoints via getAllEndpoints, and you’re done — the class directly implements RequestStreamHandler.

Uses AwsSyncServerInterpreter (AwsRequest => AwsResponse).

"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda-core" % "1.13.19"

Example: SyncLambdaApiExample

cats-effect

Extend LambdaHandler[F, R], provide your endpoints via getAllEndpoints, and implement handleRequest by calling process(input, output).unsafeRunSync().

Uses AwsCatsEffectServerInterpreter (AwsRequest => F[AwsResponse]).

"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % "1.13.19"

Examples: LambdaApiExample, V1 variant

ZIO

Create a handler instance via ZioLambdaHandler.default(endpoints), then call handler.process[AwsRequest](input, output) from a RequestStreamHandler implementation, running the ZIO effect with Runtime.default.unsafe.run(...).

Uses AwsZioServerInterpreter (AwsRequest => RIO[Env, AwsResponse]).

"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda-zio" % "1.13.19"

Example: ZioLambdaHandlerImpl

Custom runtime

As an alternative to the AWS-provided Java runtime, you can use a custom runtime where your application includes its own runtime loop that polls the Lambda Runtime API for invocations via HTTP. This can be useful when running in custom containers or with GraalVM native images. The tradeoff is an additional dependency on an HTTP client (sttp client4 with the fs2 backend).

cats-effect

Extend AwsLambdaIORuntime and provide your endpoints. The base class implements main and runs the polling loop via AwsLambdaRuntime.

Uses AwsCatsEffectServerInterpreter (AwsRequest => F[AwsResponse]).

"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % "1.13.19"

NodeJS runtime

You can also compile your Scala code to JavaScript via Scala.js and run it on Node.js. The main benefit are faster cold starts, though with AWS’s SnapStart, this might or might not be a significant advantage.

Handler functions are exported to JavaScript using @JSExportTopLevel and return a js.Promise[AwsJsResponse]. Use AwsJsRouteHandler to bridge between the JS request/response types and tapir’s Route[F]. You can use this with either Future or cats-effect:

Future

Uses AwsFutureServerInterpreter (AwsRequest => Future[AwsResponse]).

"com.softwaremill.sttp.tapir" %%% "tapir-aws-lambda-core" % "1.13.19"

Example: LambdaApiJsExample

cats-effect

Uses AwsCatsEffectServerInterpreter (AwsRequest => IO[AwsResponse]). Supports both plain Route[IO] (via catsIOHandler) and Resource[IO, Route[IO]] (via catsResourceHandler).

"com.softwaremill.sttp.tapir" %%% "tapir-aws-lambda" % "1.13.19"

Example: LambdaApiJsResourceExample

Deployment

To make it possible, to call your endpoints, you will need to deploy your application to Lambda, and configure Amazon API Gateway. Tapir leverages ways of doing it provided by AWS, you can choose from: AWS SAM template file, terraform configuration, and AWS CDK.

You can start by adding one of the following dependencies to your project, and then follow examples:

"com.softwaremill.sttp.tapir" %% "tapir-aws-sam" % "1.13.19"
"com.softwaremill.sttp.tapir" %% "tapir-aws-terraform" % "1.13.19"
"com.softwaremill.sttp.tapir" %% "tapir-aws-cdk" % "1.13.19"

Examples

Go ahead and clone tapir project. To deploy you application to AWS you will need to have an AWS account and AWS command line tools installed.

SAM

SAM can be deployed using Java runtime or NodeJS runtime. For each of these cases first you will have to install AWS SAM command line tool, and create a S3 bucket, that will be used during deployment. Before going further, open sbt shell, as it will be needed for both runtimes.

For Java runtime, use sbt to run assembly task, and then runMain sttp.tapir.serverless.aws.examples.SamTemplateExample, this will generate template.yaml sam file in main directory

For NodeJS runtime, first generate AWS Lambda yaml file by execution inside sbt shell command awsExamples/runMain sttp.tapir.serverless.aws.examples.SamJsTemplateExample, and then build Node.js module with awsExamplesJS/fastLinkJS, it will create all-in-one JS file under tapir/serverless/aws/examples/target/js-2.13/tapir-aws-examples-fastopt/main.js

From now the steps for both runtimes are the same:

  1. Before deploying, if you want to test your application locally, you will need Docker. Execute sam local start-api --warm-containers EAGER, there will be a link displayed at the console output

  2. To deploy it to AWS, run sam deploy --template-file template.yaml --stack-name sam-app --capabilities CAPABILITY_IAM --s3-bucket [name of your bucket]. The console output should print url of the application, just add /api/hello to the end of it, and you should see Hello! message. Be aware in case of Java runtime, the first call can take a little longer as the application takes some time to start, but consecutive calls will be much faster.

  3. When you want to rollback changes made on AWS, run sam delete --stack-name sam-app

Terraform

Terraform deployment requires you to have a S3 bucket.

  1. Install Terraform

  2. Run assembly task inside sbt shell

  3. Open a terminal in tapir/serverless/aws/examples/target/jvm-2.13 directory. That’s where the fat jar is saved. You need to upload it into your s3 bucket. Using command line tools: aws s3 cp tapir-aws-examples.jar s3://{your-bucket}/{your-key}.

  4. Run runMain sttp.tapir.serverless.aws.examples.TerraformConfigExample {your-aws-region} {your-bucket} {your-key} inside sbt shell

  5. Open terminal in tapir root directory, run terraform init and terraform apply

That will create api_gateway.tf.json configuration and deploy Api Gateway and lambda function to AWS. Terraform will output the url of the created API Gateway which you can call followed by /api/hello path.

To destroy all the created resources run terraform destroy.

CDK

  1. First you need to install:

  2. Open sbt shell, then run assembly task, and execute runMain sttp.tapir.serverless.aws.examples.CdkAppExample to generate CDK application template under cdk directory

  3. Go to cdk and run npm install, it will create all files needed for the deployment

  4. Before deploying, if you want to test your application locally, you will need Docker and AWS SAM command line tool , then execute cdk synth, and sam local start-api -t cdk.out/TapirCdkStack.template.json --warm-containers EAGER

  5. To deploy it to AWS simply run cdk deploy

  6. When you want to rollback changes made on AWS, run cdk destroy