npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

simple-lambda-router

v2.0.0

Published

Small router utility for lambda functions handling HTTP events from multiple resources and methods

Downloads

22

Readme

Use Case

When building an serverless application on AWS Lambda driven by events from API Gateway, there are several popular patterns:

  1. Every single HTTP endpoint and method gets its own dedicated lambda function implementing it
  2. There's a single HTTP endpoint sending all requests to one big lambda function
  3. You have a couple resources defined in API gateway which send requests to a set of lambda functions each handling a scope of your logic. For example, there could be a function handling all GET/POST/PUT/DELETE requests related to a users entity and another to handle requests for a todos entity.

For case 2 and 3 above, your lambda function will need to route the incoming request to the appropriate code to respond to it. This is what I built this little library for.

Bonus functionality

In addition to routing requests to the right handler, this package adds a few useful things:

  • Promises: Uses promises for async logic. Your handler code is expected to return a Promise when called.
  • Error handling: The router catches exceptions from the handler code result in a clean 500 error. The module also exports a RouteError class that is useful to reject requests with a specific status code.
  • Response headers: You can define default headers to be included in responses for all requests (e.g. CORS headers) and add more headers for specific requests in the handler code.
  • Input validation: You can define body and query parameters validation rules and the router will apply them. If body or query parameters don't match your validation rules, the request is rejected with appropriate error code and your handler code isn't executed. The way validation works allows setting default value for optional input.
  • Request body parsing: If the incoming request has a Content-Type header indicating the body is JSON data. It will be parsed into a JS object before it is passed to you handler code.

Changes in version 2

The functionality remains pretty much the same but the package has been refactored to work with the ES module syntax rather than CommonJS modules. If you upgrade, make sure you use import rather than require to get elements from this package and your handler files should also be ES modules.

How It Works

Lambda Function Handler

The package exports a Route function. It takes routing configuration and its return value is the handler of your lambda function. For example, if you have a Lambda function defined in your SAM template whose index property is set to index.handler as exported by code in file index.mjs, this is what that index.mjs file would look like:

Routing based on request.resource:

import { Route } from 'simple-lambda-router'
export const handler = Route(
  {
    resources: {
      '<HTTP_METHOD>:<API_RESOURCE>': <FILE_HANDLING_THAT_REQUEST>
    },
    headers: {
      '<SOME_HTTP_RESPONSE_HEADER>': '<HEADER_VALUE>'
    }
  },
  <OTHER_ARGUMENTS_TO_BE_PASSED_TO_YOUR_REQUEST_HANDLERS>
)

Routing based on request.path:

import { Route } from 'simple-lambda-router'
export const handler = Route(
  {
    paths: {
      '<HTTP_METHOD>:<API_PATH>': <FILE_HANDLING_THAT_REQUEST>
    },
    headers: {
      '<SOME_HTTP_RESPONSE_HEADER>': '<HEADER_VALUE>'
    }
  },
  <OTHER_ARGUMENTS_TO_BE_PASSED_TO_YOUR_REQUEST_HANDLERS>
)

Route Handler Files

Files defined as handlers as defined in arguments to Route() above must export a function called handler and may also export a validate object as follows:

import joi from 'joi'

export function handler(request, context, <OTHER_ARGUMENTS_FROM_CALL_TO_ROUTER.ROUTE>) {
  return new Promise((resolve, reject) => {
    resolve({
      statusCode: 200,
      body: {
        someProp: 'the value'
      },
      headers: {
        '<SOME_HTTP_RESPONSE_HEADER>': '<HEADER_VALUE>'
      }
    })
  })
}

export const validate = {
  queryStringParameters: <QUERYSTRING_VALIDATION_RULES>,
  body: <BODY_VALIDATION_RULES>,
}

handler

The handler function gets the request and context arguments as if they were handling the lambda function directly. It also gets all other arguments passed after the config in Route() (see first snippet above).

Your handler function must return a Promise. It resolves to an object that must contains a statusCode and body property, it may also contain a headers property:

  • If body is an object, it will go through JSON.stringify() before being sent back as the response body.
  • HTTP headers defined in the headers property are included in the response in addition to those included in the initial Route() call. If the same HTTP header is defined in both places, the value in the promise resolution of the handler has precedence.

If your Promise is rejected or has uncaught exceptions, the router will send a 500 Internal Server Error response back to lambda. You can reject with a specific HTTP error as follows:

import { RouteError } from 'simple-lambda-router'
export function handler(request, context) {
  return new Promise((resolve, reject) => {
    reject(new RouteError({
      statusCode: 403,
      message: 'Check with your admin to get access to this.'
    }))
  })
}

validate

The validate object lets you define validation rules for the request body and/or query parameters. Validation is super easy to use with the Joi Object schema validation library but you can use anything as long as you wrap it in a validate method compatible with the Joi validate method.

import joi from 'joi'

export const handler = (request, context) => {
  ...
}

export const validate = {
  // Validating using Joi library
  queryStringParameters: joi.object({
    status: joi.string().valid('opened','completed').required(),
  }),
  // Custom body validation/augment code
  body: {
    validate: (requestBody) => {
      let error = null
      if (!('requiredProp' in requestBody)) {
        error = {
          details: [
            { message: 'Request body must contain a "requiredProp" property.' }
          ]
        }
      }
      let value = Object.assign({}, { propWithDefaultVal: 'default val' }, requestBody)
      return { error, value }
    }
  }
}

Routing based on request.path

If your API gateway has a proxy resource with a greedy path variable {proxy+}, the request.resource value will be that greedy path and the request.path will be the actual path with no extracted parameters showing in request.pathParameters.

Example:

Your API declares a /{proxy+} resource and receives a GET /items/34. Upon receiving that request, API Gateway sends an object to your Lambda function with a resource property set to /{proxy+} and path property set to /items/34.

This package allows you to define path-based routing. In other words, the routing logic looks at /items/34 from the example above, not /{proxy+} as it would with resource-based routing. Note that your path route can contain named parameter the same way they are defined in an API Gateway resource path. Those parameters will be extracted and added in the pathParameters property of the request object sent to your handler.

Example [ctnd]:

A path key of GET:/items/{id} will match the request from the example and your handler will have request.pathParameters.id set to 34

Chaining handlers

It is possible to handle requests to a path or resource through a chain of handlers instead of a single handler file. This is useful if many endpoints have common validations (e.g. handlers for /items/4, /items/4/links, and /items/4/foo would all need to check if item 4 actually exist and return a 404 if it doesn't).

You chain handlers by setting the handlers as an array of files. Each step in the handler chain follows the same structure as a single-file handler (it returns an object with a handler function returning a Promise and an optional validate object). Validation rules are enforced for each step of the chain and each step gets the value the previous step resolves to as one if its arguments.

See examples under items-fn or the test-07-chained-handlers.js file for more details.

Examples

Example in the examples directory are for the previous version of this package which followed CommonJS modules syntax. Looking at tests may be more useful.