AWS API Gateway Router
Find the full documentation at https://pushplay.github.io/cassava/
Cassava is your typical NPM package.
npm install --save cassava
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 (read on for more details)
- 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 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"]}!`
};
});
// A very fancy route using regex.
// Matches all paths that start with `/hello` and more non-slash characters.
router.route(/\/hello([^/]+)/)
.method("GET")
.handler(async evt => {
return {
body: `Hello ${evt.pathParameters["2"]}!`
};
});
// Install the router as the handler for this lambda.
export const handler = router.getLambdaHandler();
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 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.
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:
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.
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 comes with a number of utility functions to validate the event.
blacklistQueryStringParameters(...params: string[])
disallow any of the given query parametersrequireHeader(field: string)
require that a header is setrequireHeader(field: string, values: string[], explanation?: string)
require that a header is set and takes one of a given list of valuesrequireHeader(field: string, validator: function, explanation?: string)
require that a header is set and satisfies the validator functionrequireQueryStringParameter(param: string)
require that a query parameter is setrequireQueryStringParameter(param: string, values: string[], explanation?: string)
require that a query parameter is set and takes one of a given list of valuesrequireQueryStringParameter(param: string, validator: function, explanation?: string)
require that a query parameter is set and satisfies the validator functionvalidateBody(schema: Schema, options?: ValidateBodyOptions)
validate the request body using JSON SchemawhitelistQueryStringParameters(...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();
Cassava is a starchy root vegetable grown all over the world. The more you know. ┈┅*