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();
router.route("/helloWorld")
.method("GET")
.handler(async evt => {
return {
body: "Hello world!"
};
});
router.route("/hello/{name}")
.method("GET")
.handler(async evt => {
return {
body: `Hello ${evt.pathParameters["name"]}!`
};
});
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();
router.route(new cassava.routes.LoggingRoute());
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;
return {
statusCode: 204
};
}
});
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 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();
router.route("/locations/{locationId}")
.method("GET")
.handler(async evt => {
evt.whitelistQueryStringParameters();
return {
body: getLocationById(evt.pathParameters.locationId)
};
});
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)
};
});
router.route("/locations")
.method("GET")
.handler(async evt => {
evt.requireQueryStringParameter("query");
return {
body: getLocationsByQuery(evt.queryStringParameters.query)
};
});
export const handler = router.getLambdaHandler();
The Name
Cassava is a starchy root vegetable grown all over the world. The more you know. ┈┅*