Test your HTTP lambdas locally

ServerlessJS takes one hundred thousand years to deploy small changes, so it's faster to test locally before shipping to dev.

Also, 3rd party libraries are the devil, so it's often better to roll your own.

The code below is copy-paste-able into your Serverless project to keep your build small, and to keep your project safe.

What are you hiding?

This file itself requires some 3rd party devil libraries

Not everything is tested or implemented

The setup

Add your routes to the "router" object near the top of the file.

{
    '/the/api/path': theHandlerFunction,
    '/{parameters}/also/work': aFunctionThatTakesParameters,
}

By default this runs on port 8888, but that can be changed by updating the value in listen(8888) near the bottom.

This does what now?

This setups a server that

  1. Receives an HTTP request over http://localhost:8888
  2. Matches the request path with a router map
  3. Parses out any path parameters like http://localhost:8888/user/1234 -> http://localhost:8888/user/{userId} -> {userId: 1234}
  4. Passes the request to a handler function using the same signature as AWS Api Gateway (handler(req: APIGatewayEvent))

The code

import http from "http";
import { info, error } from "console";

import AiInsightsCms from './lambdas/reports/ai-insights-cms.local';
import { APIGatewayEvent } from "aws-lambda";

// Add your routes here, note that we're blissfully
// ignoring HTTP method
const router = {
  '/ai-insights/{websiteId}': AiInsightsCms
}

// Awaitable helper to parse the request
// body stream into a string
function parseBody(r: http.IncomingMessage) {
  return new Promise((resolve) => {
    const chunks = [] as Buffer[];
    r.on('data', d => {
      chunks.push(d);
    }).on('end', () => {
      resolve(Buffer.concat(chunks).toString())
    });
  });
}

// Create the HTTP listener
http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
  info(`Received request: ${req.method} ${req.url}`);

  try {
    // Parse the request
    const uri = new URL(req.url!, `http://${req.headers.host}`);

    // Initialize variables to hold the handler and params
    // in case we find a matching route
    let handler: Function | null = null;
    let params: { [key: string]: string } = {};

    // Iterate each configured route and extract the params 
    // and handler if one is found that matches the request
    for (const [route, handlerFn] of Object.entries(router)) {
      // Sub out the route parameters with regex
      const regex = new RegExp('^' + route.replace(/{[^}]+}/g, '([^/]+)') + '$');
      const match = uri.pathname.match(regex);

      if (match) {
        // Extract the parameter names stripping off the {} with slice(1,-1)
        const paramNames = (route.match(/{[^}]+}/g) || []).map(name => {
          return name.slice(1, -1);
        });
        // Map the parameter names to their values
        paramNames.forEach((name, index) => {
          params[name] = match[index + 1];
        });

        // Save the handler function and stop iterating
        handler = handlerFn;
        break;
      }
    };

    // Return 404 if no handler found
    if (!handler) {
      res.writeHead(404, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Not Found' }));
      return;
    }

    // Build a mock API Gateway event
    // with the path parameters and body
    // parsed from the request
    const apiGatewayReq = {
      httpMethod: req.method!,
      headers: req.headers,
      path: req.url!,
      pathParameters: params,
      body: await parseBody(req)
    } as APIGatewayEvent;

    // Call the handler function
    const handlerResp = await handler(apiGatewayReq);

    info(`Success - handler response:`, handlerResp);
    res.writeHead(200);
    res.write(JSON.stringify(handlerResp, null, 4));
    res.end();
  } catch (e) {
    error('Error handling request', e);
    res.writeHead(500, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'Internal Server Error' }));
  }
}).listen(8888);

info("Static file server running at\n  => http://localhost:8888/\nCTRL + C to shutdown");

How to run

yarn ts-node local-server.ts

# But I have envvars
VARIABLE1=test yarn ts-node local-server.ts

Or if you have VS Code, just copy this launch config and press F5. Envvars are handles using the dotenv package.

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node-terminal",
      "request": "launch",
      "name": "Run local API Gateway server",
      "command": "yarn ts-node local-server.ts",
      "envFile": "${workspaceFolder}/.env"
    }
  ]
}