DEV Community

Thomas Taylor for AWS Community Builders

Posted on • Originally published at how.wtf

Deploy serverless Lambda TypeScript API with function url using AWS CDK

In November 2023, I wrote a post describing how to deploy a lambda function with a function url in Python. For this post, I want to showcase how streamlined and practical it is to deploy a "Lambdalith" (a single Lambda function) that contains an entire API.

What this means:

  1. No API Gateway
  2. API requests can take longer than 30 seconds
  3. Faster deployments
  4. Local testing without cloud deployment
  5. Reduced costs *
  6. Easy management *

* = Depends on the use-case

How to deploy a serverless API using Fastify

To begin, let's initialize a CDK application for Typescript:

cdk init app --language typescript
Enter fullscreen mode Exit fullscreen mode

This creates the boilerplate directories and files we'll need:

serverless-api
├── README.md
├── bin
│   └── serverless-api.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── serverless-api-stack.ts
├── package-lock.json
├── package.json
├── test
│   └── serverless-api.test.ts
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Install and configure Fastify

Fastify is a JavaScript web framework that intentionally aims for low overhead and speed over other frameworks such as express. I have arbitrarily chose it for this tutorial, but any web framework that supports routing will work.

Install fastify

Install fastify using one of the methods described in their documentation and their AWS Lambda adapter @fastify/aws-lambda.

For this tutorial, I'll use npm.

npm i fastify @fastify/aws-lambda @types/aws-lambda
Enter fullscreen mode Exit fullscreen mode

Create an entry file

To make it easy, we'll create an entry point for the lambda at handler/index.ts with the following contents:

import Fastify from "fastify";
import awsLambdaFastify from "@fastify/aws-lambda";

function init() {
  const app = Fastify();
  app.get('/', (request, reply) => reply.send({ hello: 'world' }));
  return app;
}

if (require.main === module) {
  // called directly i.e. "node app"
  init().listen({ port: 3000 }, (err) => {
    if (err) console.error(err);
    console.log('server listening on 3000');
  });
} else {
  // required as a module => executed on aws lambda
  exports.handler = awsLambdaFastify(init())
}
Enter fullscreen mode Exit fullscreen mode

The directory structure should look like the following tree:

serverless-api
├── README.md
├── bin
│   └── serverless-api.ts
├── cdk.json
├── handler
│   └── index.ts
├── jest.config.js
├── lib
│   └── serverless-api-stack.ts
├── package-lock.json
├── package.json
├── test
│   └── serverless-api.test.ts
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

With this method, we are able to test locally without deploying to the cloud.

First, transpile the Typescript files to JavaScript:

npm run build
Enter fullscreen mode Exit fullscreen mode

Then execute the handler/index.js file with node:

node handler/index.js
Enter fullscreen mode Exit fullscreen mode

If you visit http://localhost:3000 in your browser, it should display:

{
    "hello": "world"
}
Enter fullscreen mode Exit fullscreen mode

Deploying the function with the function url enabled

Fortunately, the AWS CDK enables users to quickly deploy using the NodeJSFunction construct. Replace the contents of serverless-api-stack.ts with the following snippet:

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { FunctionUrlAuthType } from 'aws-cdk-lib/aws-lambda';

export class ServerlessApiStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const handler = new NodejsFunction(this, 'handler', {
      entry: './handler/index.ts',
      timeout: cdk.Duration.minutes(1)
    });
    const url = handler.addFunctionUrl({
      authType: FunctionUrlAuthType.NONE
    });
    new cdk.CfnOutput(this, 'url', {
      value: url.url
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The code creates a NodejsFunction lambda, enables the function url without authentication, and outputs the url as a CloudFormation export.

Deploy the stack using the cdk:

npx cdk deploy
Enter fullscreen mode Exit fullscreen mode

The command output contains the CfnOutput value:

Do you wish to deploy these changes (y/n)? y
ServerlessApiStack: deploying... [1/1]
ServerlessApiStack: creating CloudFormation changeset...

 ✅  ServerlessApiStack

✨  Deployment time: 38.19s

Outputs:
ServerlessApiStack.url = https://{id}.lambda-url.us-east-1.on.aws/
Stack ARN:
arn:aws:cloudformation:us-east-1:123456789012:stack/ServerlessApiStack/{id}
Enter fullscreen mode Exit fullscreen mode

If you navigate to the url, you will view the expected results displayed:

{
    "hello": "world"
}
Enter fullscreen mode Exit fullscreen mode

All of this was completed with very little infrastructure to manage and a single index.ts file. From here, you can expand the project to include as many routes as you prefer.

Top comments (6)

Collapse
 
shotlom profile image
Sholto Maud

Local testing is great. But why the addFunctionUrl?

If we know it works locally what benefit does addFunctionUrl provide? Wouldn't I want to test the API GW since that will be the end target state?

Sorry what am I missing?

Collapse
 
therealdakotal profile image
Dakota Lewallen

In this instance, the main benefit is direct access to the lambda code. You don't have to worry about DNS config, API config, or other details. Using this setup you can greatly reduce the overhead of getting something spun up. As well as reduce the debugging flywheel, as there are significantly fewer things you have to be concerned with.

Is this something you would point your users to? No. Is this something you would point the developers to? Yes.

Collapse
 
thomastaylor profile image
Thomas Taylor • Edited

I agree with your points here, Dakota.

One caveat:

Is this something you would point your users to? No.

I'm advocating that you optionally can point your users to an endpoint that is served by the function url; however, I would recommend leveraging CloudFlare DNS or something similar in front.

Collapse
 
thomastaylor profile image
Thomas Taylor • Edited

Hey Sholto!

Thank you for your inquiry. In this architecture, we are not using Amazon API Gateway. The intention is to deploy a single lambda with a function URL as opposed to linking an API Gateway with a proxy lambda handler. So, the function url is used instead of an API Gateway.

To answer your question, the function url enables the lambda to be accessible via URL. Function urls also support custom domains.

Does this make sense?

Collapse
 
shotlom profile image
Sholto Maud

Yes I understand. But I don't understand why you don't use api gw. My assumption was that you're not going to abandon api gw. Or is your final app design something without api gw at all?

Thread Thread
 
thomastaylor profile image
Thomas Taylor

The final design is without an API Gateway in this context.

If you want AWS-provided caching, throttling, authentication, authorization, etc., use an API Gateway. If you want to handle those within your application layer (lambda), then you can just get away with a function url. It removes the overhead of an API Gateway.