Nested API routes with Node.js AWS Lambda Function URL, Middy HTTP router and AWS CDK

Nested API routes with Node.js AWS Lambda Function URL, Middy HTTP router and AWS CDK

service

By Andrzej Komarnicki
Cloud DevOps @ smallfries.digital
Date: 2023-04-17

With the rising popularity of Function URLs, which are dedicated HTTP(S) endpoints for your Lambda function, we can avoid many of the complexities, costs and limitations associated with Amazon API Gateway. The downside is we also lose some of the nice features of an API Gateway that we get out of the box, one key feature being API routing based on method and path of an http event. This is where Middy, a Node.js middleware engine for AWS Lambda really comes in handy.

In particular, we can achieve nested http routing within our Lambdas by leveraging Middy's http-router handler. Let's start by cloning the starter project repo:

git clone https://github.com/AndrzejKomarnicki/awscdk-nodejsfunction-middy

Next let's install the npm packages and run some AWS CDK CLI commands from the root working directory of our cloned repo.

npm install
cdk synth && cdk diff

By default AWS CDK CLI will check your environment variables for your AWS creds and region first, if not found it will default back to your AWS credentials and config files. You can of course also specify the AWS CLI profile of choice. For more info on how to setup environments for your Stack instance head over to https://docs.aws.amazon.com/cdk/v2/guide/environments.html.

Anyway, if everything checks out let's proceed to deploy the Stack as part of our CDK app:

cdk deploy

After deployment, you should see output similar to below:

functionurl

Next let's dive into the Lambda function source code before we test the endpoints...
Open up index.ts from /services/node-lambda/

import middy from "@middy/core";
import jsonBodyParser from "@middy/http-json-body-parser";
import httpErrorHandler from "@middy/http-error-handler";
import httpSecurityHeaders from "@middy/http-security-headers";
import httpRouterHandler from "@middy/http-router";
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
import { Logger, injectLambdaContext } from "@aws-lambda-powertools/logger";

const logger = new Logger({
  logLevel: "INFO",
  serviceName: "middy-example-api",
});

async function getHandler(
  event: APIGatewayProxyEventV2,
  context: any
): Promise<APIGatewayProxyResultV2> {
  // the returned response will be checked against the type `APIGatewayProxyResultV2`
  logger.info("This is a INFO log with some context");
  console.log("event 👉", event);
  return {
    statusCode: 200,
    body: JSON.stringify(`Hello from ${event.rawPath}`),
  };
}

async function postHandler(
  event: APIGatewayProxyEventV2,
  context: any
): Promise<APIGatewayProxyResultV2> {
  // the returned response will be checked against the type `APIGatewayProxyResultV2`
  logger.info("This is a INFO log with some context");
  console.log("event 👉", event);
  return {
    statusCode: 200,
    body: JSON.stringify(event.body),
  };
}

// routes served by httpRouterHandler middleware
// you can add more nested handlers for routes (method and path) as needed
const routes = [
  {
    method: "GET",
    path: "/user/{id}",
    handler: getHandler,
  },
  {
    method: "POST",
    path: "/user",
    handler: postHandler,
  },
];

export const handler = middy()
  .use(jsonBodyParser())
  .use(httpSecurityHeaders())
  .use(httpErrorHandler())
  .use(injectLambdaContext(logger)) // Change to (logger, { logEvent: true }) to log the incoming event
  .handler(httpRouterHandler(routes));

As you can tell we import httpRouterHandler from @middy/http-router, and we define two async node.js functions - getHandler() and postHandler(), which are referenced by the routes object as part of its handler property depending on http path and method in the http event.

Of course you can re-organize these nested Lambda handlers to be exported as modules instead of having everything in one monolithic index.ts file, this only serves as a starting point for demonstration purposes.

Let's proceed to test our endpoints with the different paths and methods we defined. You can go back to the outputs generated by "CDK deploy" and test against both the Function URL and CloudFront URL.

GET method on /user/{userid}

getmethod

POST method on /user

postmethod