cassava

AWS API Gateway Router

Usage no npm install needed!

<script type="module">
  import cassava from 'https://cdn.skypack.dev/cassava';
</script>

README

Cassava

AWS API Gateway Router

Find the full documentation at https://giftbit.github.io/cassava/

Routing

There are two ways to add routes to Cassava.

  • route(string|RegExp)
    • simplest method that handles most cases
    • string routes are case insensitive and support path parameters
    • RegExp routes place matching groups in the path parameters
  • route(Route)
    • provides the most flexibility
    • can modify responses before they are sent
    • is the most work to implement

Cassava processes REST events by examining installed routes from top-to-bottom. Cassava works downwards to find the first route that matches and responds, and then works back up to do any post-processing.

A route responds when: it matches the event, has a handle function, and handles the event by returning a response object or Promise that resolves to a response object.

A route can post-process the response when: it matches the event, did not return a response object in handle, and has a postProcess function. Post processing can be used to modify the response or cause some side effect such as logging.

RouteBuilder

RouteBuilder is the simplest way to add a route to Cassava. A RouteBuilder instance is started with router.route(string) or router.route(RegExp), then with chained function calls you can specify the HTTP method, add a handle function or a postProcess function.

The details of handling and post-processing are covered later in this document.

For example...

import * as cassava from "cassava";

const router = new cassava.Router();

// A simple route that only handles GET on /helloWorld
router.route("/helloWorld")
    .method("GET")
    .handler(async evt => {
        return {
            body: "Hello world!"
        };
    });

// A fancier route with a path parameter `name`.
// match egs: `/hello/jeff` or `/Hello/Jeffery`
router.route("/hello/{name}")
    .method("GET")
    .handler(async evt => {
        return {
            body: `Hello ${evt.pathParameters["name"]}!`
        };
    });

// Install the router as the handler for this lambda.
export const handler = router.getLambdaHandler();

Custom Routes

A custom route is one that implements the Route interface: it must have a matches function that accepts a RouterEvent and returns a boolean and at least one of: a handle function or postProcess function.

The details of RouterEvents, handling and post-processing are covered later in this document.

For example...

import * as cassava from "cassava";

const router = new cassava.Router();

// A custom route that comes with Cassava providing console logging of requests.
// This route has both a `handle` and `postProcess` function to log both requests and responses.
router.route(new cassava.routes.LoggingRoute());

// A custom Route that handles PUT or PATCH on any path starting with /upload/
router.route({
    matches: evt => {
        return (evt.httpMethod === "PUT" || evt.httpMethod === "PATCH") &&
            evt.path.startsWith("/upload/");
    },
    handle: async evt => {
        const fileName = evt.path.substring("/upload/".length);
        const fileContents = evt.body;
        // ... store fileContents with fileName
        return {
            statusCode: 204
        };
    }
});

// Install the router as the handler for this lambda.
export const handler = router.getLambdaHandler();

RouterEvents, RouterResponses, handling and postProcessing

RouterEvents are the input to matches and handle functions. They fully describe all information about the REST request including the full body as streaming is not supported.

A handle function takes in a RouterEvent and can return the following: null or undefined to not handle the RouterEvent in which case further routes are consulted; a RouterResponse that represents the response sent to the client; a Promise that resolves to null or undefined which will again let further routes handle the request; a Promise that resolves to a RouterResponse which again will be the response sent to the client.

RouterResponses include the body, an optional HTTP status code (defaults to 200), and optionally any headers that might be set.

A postProcess function takes in both the RouterEvent and the current RouterResponse. It can return null or undefined or a Promise resolving to one of those to not affect the final response; or it can return a RouterResponse or a Promise resolving to a RouterResponse to change the response.

Response serialization

The default assumption is that you're building a JSON-based API so that's the simplest case. By default the response body will be JSON stringified and the header Content-Type set to application/json. This is true even if the body is a string. If you don't want that behavior you have two options:

Manual Content-Type

The first option for returning non-JSON is to set the response body to a string or Buffer, and set the Content-Type header. This works when using a custom route or the route builder. For example:

router.route("/robots")
    .method("GET")
    .handler(async evt => {
        return {
            headers: {
                "Content-Type": "text/csv"
            },
            body: "robot,film\nRobby,Forbidden Planet\nGort,The Day the Earth Stood Still"
        };
    });

This is simple to implement but ignores the client's Accept header. This endpoint will always return csv regardless of what the client asks for.

RouteBuilder.serializers

When using the route builder there is a second option of letting the handler return a complex object as in the JSON case, but defining serializer functions for each response mime type. The appropriate serializer will be chosen based upon the client's Accept header. In the following example the same endpoint can return one of JSON, CSV and XML.

router.route("/robots")
    .method("GET")
    .serializers({
        "application/json": cassava.serializers.json,
        "text/csv": body => new json2csv.Parser({fields: ["robot", "film"]}).parse(body),
        "application/xml": body => jsontoxml({robots: body})
    })
    .handler(async evt => {
        return {
            body: [
              {
                  robot: "Robby",
                  film: "Forbidden Planet"
              },
              {
                  robot: "Gort",
                  film: "The Day the Earth Stood Still"
              }
            ]
        };
    });

In this example CSV serialization is handled by json2csv and XML serialization by jsontoxml. These libraries are not included with Cassava and you're free to choose your own serialization libraries.

RouterEvent Validation

RouterEvent comes with a number of utility functions to validate the event.

  • blacklistQueryStringParameters(...params: string[]) disallow any of the given query parameters
  • requireHeader(field: string) require that a header is set
  • requireHeader(field: string, values: string[], explanation?: string) require that a header is set and takes one of a given list of values
  • requireHeader(field: string, validator: function, explanation?: string) require that a header is set and satisfies the validator function
  • requireQueryStringParameter(param: string) require that a query parameter is set
  • requireQueryStringParameter(param: string, values: string[], explanation?: string) require that a query parameter is set and takes one of a given list of values
  • requireQueryStringParameter(param: string, validator: function, explanation?: string) require that a query parameter is set and satisfies the validator function
  • validateBody(schema: Schema, options?: ValidateBodyOptions) validate the request body using JSON Schema
  • whitelistQueryStringParameters(...params: string[]) disallow any query parameters other than the ones set

An example:

import * as cassava from "cassava";

const router = new cassava.Router();

// Get a location
router.route("/locations/{locationId}")
    .method("GET")
    .handler(async evt => {
        evt.whitelistQueryStringParameters();   // don't allow any query params
        return {
            body: getLocationById(evt.pathParameters.locationId)
        };
    });

// Set a location
router.route("/locations/{locationId}")
    .method("POST")
    .handler(async evt => {
        evt.validateBody({
             type: "object",
             properties: {
                 latitude: { "type": "number" },
                 longitude: { "type": "number" }
             },
             required: ["latitude", "longitude"]
         });
        return {
            body: setLocationId(evt.pathParameters.locationId, evt.body)
        };
    });

// Query for locations
router.route("/locations")
    .method("GET")
    .handler(async evt => {
        evt.requireQueryStringParameter("query");
        return {
            body: getLocationsByQuery(evt.queryStringParameters.query)
        };
    });

// Install the router as the handler for this lambda.
export const handler = router.getLambdaHandler();

The Name

Cassava is a starchy root vegetable grown all over the world. The more you know. ┈┅*