simple-lambda-api
v0.1.0
Published
Tiny web framework for single-route serverless functions, based on Lambda API
Downloads
1
Maintainers
Readme
Lightweight web framework for your serverless applications
Lambda API is a lightweight web framework for AWS Lambda using AWS API Gateway Lambda Proxy Integration or ALB Lambda Target Support. This closely mirrors (and is based on) other web frameworks like Express.js and Fastify, but is significantly stripped down to maximize performance with Lambda's stateless, single run executions.
Simple Example
// Require the framework and instantiate it
const api = require('lambda-api')();
// Define a route
api.get('/status', async (req, res) => {
return { status: 'ok' };
});
// Declare your Lambda handler
exports.handler = async (event, context) => {
// Run the request
return await api.run(event, context);
};
For a full tutorial see How To: Build a Serverless API with Serverless, AWS Lambda and Lambda API.
Why Another Web Framework?
Express.js, Fastify, Koa, Restify, and Hapi are just a few of the many amazing web frameworks out there for Node.js. So why build yet another one when there are so many great options already? One word: DEPENDENCIES.
These other frameworks are extremely powerful, but that benefit comes with the steep price of requiring several additional Node.js modules. Not only is this a bit of a security issue (see Beware of Third-Party Packages in Securing Serverless), but it also adds bloat to your codebase, filling your node_modules
directory with a ton of extra files. For serverless applications that need to load quickly, all of these extra dependencies slow down execution and use more memory than necessary. Express.js has 30 dependencies, Fastify has 12, and Hapi has 17! These numbers don't even include their dependencies' dependencies.
Lambda API has ZERO dependencies. None. Zip. Zilch.
Lambda API was written to be extremely lightweight and built specifically for SERVERLESS applications using AWS Lambda and API Gateway. It provides support for API routing, serving up HTML pages, issuing redirects, serving binary files and much more. Worried about observability? Lambda API has a built-in logging engine that can even periodically sample requests for things like tracing and benchmarking. It has a powerful middleware and error handling system, allowing you to implement just about anything you can dream of. Best of all, it was designed to work with Lambda's Proxy Integration, automatically handling all the interaction with API Gateway for you. It parses REQUESTS and formats RESPONSES, allowing you to focus on your application's core functionality, instead of fiddling with inputs and outputs.
Single Purpose Functions
You may have heard that a serverless "best practice" is to keep your functions small and limit them to a single purpose. I generally agree since building monolith applications is not what serverless was designed for. However, what exactly is a "single purpose" when it comes to building serverless APIs and web services? Should we create a separate function for our "create user" POST
endpoint and then another one for our "update user" PUT
endpoint? Should we create yet another function for our "delete user" DELETE
endpoint? You certainly could, but that seems like a lot of repeated boilerplate code. On the other hand, you could create just one function that handled all your user management features. It may even make sense (in certain circumstances) to create one big serverless function handling several related components that can share your VPC database connections.
Whatever you decide is best for your use case, Lambda API is there to support you. Whether your function has over a hundred routes, or just one, Lambda API's small size and lightning fast load time has virtually no impact on your function's performance. You can even define global wildcard routes that will process any incoming request, allowing you to use API Gateway or ALB to determine the routing. Yet despite its small footprint, it gives you the power of a full-featured web framework.
Table of Contents
- Simple Example
- Why Another Web Framework?
- Table of Contents
- Installation
- Requirements
- Configuration
- Recent Updates
- Routes and HTTP Methods
- Returning Responses
- Route Prefixing
- Debugging Routes
- REQUEST
- RESPONSE
- status(code)
- sendStatus(code)
- header(key, value [,append])
- getHeader(key [,asArray])
- getHeaders()
- hasHeader(key)
- removeHeader(key)
- getLink(s3Path [, expires] [, callback])
- send(body)
- json(body)
- jsonp(body)
- html(body)
- type(type)
- location(path)
- redirect([status,] path)
- cors([options])
- error([code], message [,detail])
- cookie(name, value [,options])
- clearCookie(name [,options])
- etag([boolean])
- cache([age] [, private])
- modified(date)
- attachment([filename])
- download(file [, filename] [, options] [, callback])
- sendFile(file [, options] [, callback])
- Enabling Binary Support
- Path Parameters
- Wildcard Routes
- Logging
- Middleware
- Clean Up
- Error Handling
- Namespaces
- CORS Support
- Compression
- Execution Stacks
- Lambda Proxy Integration
- ALB Integration
- Configuring Routes in API Gateway
- Reusing Persistent Connections
- TypeScript Support
- Contributions
- Are you using Lambda API?
Installation
npm i lambda-api --save
Requirements
- AWS Lambda running Node 8.10+
- AWS API Gateway using Proxy Integration
Configuration
Require the lambda-api
module into your Lambda handler script and instantiate it. You can initialize the API with the following options:
| Property | Type | Description |
| -------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| base | String
| Base path for all routes, e.g. base: 'v1'
would prefix all routes with /v1
|
| callbackName | String
| Override the default callback query parameter name for JSONP calls |
| logger | boolean
or object
| Enables default logging or allows for configuration through a Logging Configuration object. |
| mimeTypes | Object
| Name/value pairs of additional MIME types to be supported by the type()
. The key should be the file extension (without the .
) and the value should be the expected MIME type, e.g. application/json
|
| serializer | Function
| Optional object serializer function. This function receives the body
of a response and must return a string. Defaults to JSON.stringify
|
| version | String
| Version number accessible via the REQUEST
object |
| errorHeaderWhitelist | Array
| Array of headers to maintain on errors |
// Require the framework and instantiate it with optional version and base parameters
const api = require('lambda-api')({ version: 'v1.0', base: 'v1' });
Recent Updates
For detailed release notes see Releases.
v0.11: API Gateway v2 payload support and automatic compression
Lambda API now supports API Gateway v2 payloads for use with HTTP APIs. The library automatically detects the payload, so no extra configuration is needed. Automatic compression has also been added and supports Brotli, Gzip and Deflate.
v0.10: ALB support, method-based middleware, and multi-value headers and query string parameters
Lambda API now allows you to seamlessly switch between API Gateway and Application Load Balancers. New execution stacks enables method-based middleware and more wildcard functionality. Plus full support for multi-value headers and query string parameters.
Routes and HTTP Methods
Routes are defined by using convenience methods or the METHOD
method. There are currently eight convenience route methods: get()
, post()
, put()
, patch()
, delete()
, head()
, options()
and any()
. Convenience route methods require an optional route and one or more handler functions. A route is simply a path such as /users
. If a route is not provided, then it will default to /*
and will execute on every path. Handler functions accept a REQUEST
, RESPONSE
, and optional next()
argument. These arguments can be named whatever you like, but convention dictates req
, res
, and next
.
Multiple handler functions can be assigned to a path, which can be used to execute middleware for specific paths and methods. For more information, see Middleware and Execution Stacks.
Examples using convenience route methods:
api.get('/users', (req,res) => {
// do something
})
api.post('/users', (req,res) => {
// do something
})
api.delete('/users', (req,res) => {
// do something
})
api.get('/users',
(req,res,next) => {
// do some middleware
next() // continue execution
}),
(req,res) => {
// do something
}
)
api.post((req,res) => {
// do something for ALL post requests
})
Additional methods are support by calling METHOD
. Arguments must include an HTTP method (or array of methods), an optional route, and one or more handler functions. Like the convenience methods above, handler functions accept a REQUEST
, RESPONSE
, and optional next
argument.
api.METHOD('trace','/users', (req,res) => {
// do something on TRACE
})
api.METHOD(['post','put'],'/users', (req,res) => {
// do something on POST -or- PUT
})
api.METHOD('get','/users',
(req,res,next) => {
// do some middleware
next() // continue execution
}),
(req,res) => {
// do something
}
)
All GET
methods have a HEAD
alias that executes the GET
request but returns a blank body
. GET
requests should be idempotent with no side effects. The head()
convenience method can be used to set specific paths for HEAD
requests or to override default GET
aliasing.
Routes that use the any()
method or pass ANY
to api.METHOD
will respond to all HTTP methods. Routes that specify a specific method (such as GET
or POST
), will override the route for that method. For example:
api.any('/users', (req, res) => {
res.send('any');
});
api.get('/users', (req, res) => {
res.send('get');
});
A POST
to /users
will return "any", but a GET
request would return "get". Please note that routes defined with an ANY
method will override default HEAD
aliasing for GET
routes.
Returning Responses
Lambda API supports both callback-style
and async-await
for returning responses to users. The RESPONSE object has several callbacks that will trigger a response (send()
, json()
, html()
, etc.) You can use any of these callbacks from within route functions and middleware to send the response:
api.get('/users', (req, res) => {
res.send({ foo: 'bar' });
});
You can also return
data from route functions and middleware. The contents will be sent as the body:
api.get('/users', (req, res) => {
return { foo: 'bar' };
});
Async/Await
If you prefer to use async/await
, you can easily apply this to your route functions.
Using return
:
api.get('/users', async (req, res) => {
let users = await getUsers();
return users;
});
Or using callbacks:
api.get('/users', async (req, res) => {
let users = await getUsers();
res.send(users);
});
Promises
If you like promises, you can either use a callback like res.send()
at the end of your promise chain, or you can simply return
the resolved promise:
api.get('/users', (req, res) => {
getUsers().then((users) => {
res.send(users);
});
});
OR
api.get('/users', (req, res) => {
return getUsers().then((users) => {
return users;
});
});
IMPORTANT: You must either use a callback like res.send()
OR return
a value. Otherwise the execution will hang and no data will be sent to the user. Also, be sure not to return undefined
, otherwise it will assume no response.
A Note About Flow Control
While callbacks like res.send()
and res.error()
will trigger a response, they will not necessarily terminate execution of the current route function. Take a look at the following example:
api.get('/users', (req, res) => {
if (req.headers.test === 'test') {
res.error('Throw an error');
}
return { foo: 'bar' };
});
The example above would not have the intended result of displaying an error. res.error()
would signal Lambda API to execute the error handling, but the function would continue to run. This would cause the function to return
a response that would override the intended error. In this situation, you could either wrap the return in an else
clause, or a cleaner approach would be to return
the call to the error()
method, like so:
api.get('/users', (req, res) => {
if (req.headers.test === 'test') {
return res.error('Throw an error');
}
return { foo: 'bar' };
});
res.error()
does not have a return value (meaning it is undefined
). However, the return
tells the function to stop executing, and the call to res.error()
handles and formats the appropriate response. This will allow Lambda API to properly return the expected results.
Route Prefixing
Lambda API makes it easy to create multiple versions of the same api without changing routes by hand. The register()
method allows you to load routes from an external file and prefix all of those routes using the prefix
option. For example:
// handler.js
const api = require('lambda-api')();
api.register(require('./routes/v1/products'), { prefix: '/v1' });
api.register(require('./routes/v2/products'), { prefix: '/v2' });
module.exports.handler = (event, context, callback) => {
api.run(event, context, callback);
};
// routes/v1/products.js
module.exports = (api, opts) => {
api.get('/product', handler_v1);
};
// routes/v2/products.js
module.exports = (api, opts) => {
api.get('/product', handler_v2);
};
Even though both modules create a /product
route, Lambda API will add the prefix
to them, creating two unique routes. Your users can now access:
/v1/product
/v2/product
You can use register()
as many times as you want AND it is recursive, so if you nest register()
methods, the routes will build upon each other. For example:
module.exports = (api, opts) => {
api.get('/product', handler_v1);
api.register(require('./v2/products.js'), { prefix: '/v2' });
};
This would create a /v1/product
and /v1/v2/product
route. You can also use register()
to load routes from an external file without the prefix
. This will just add routes to your base
path. NOTE: Prefixed routes are built off of your base
path if one is set. If your base
was set to /api
, then the first example above would produce the routes: /api/v1/product
and /api/v2/product
.
Debugging Routes
Lambda API has a routes()
method that can be called on the main instance that will return an array containing the METHOD
and full PATH
of every configured route. This will include base paths and prefixed routes. This is helpful for debugging your routes.
const api = require('lambda-api')();
api.get('/', (req, res) => {});
api.post('/test', (req, res) => {});
api.routes(); // => [ [ 'GET', '/' ], [ 'POST', '/test' ] ]
You can also log the paths in table form to the console by passing in true
as the only parameter.
const api = require('lambda-api')()
api.get('/', (req,res) => {})
api.post('/test', (req,res) => {})
api.routes(true)
// Outputs to console
╔═══════════╤═════════════════╗
║ METHOD │ ROUTE ║
╟───────────┼─────────────────╢
║ GET │ / ║
╟───────────┼─────────────────╢
║ POST │ /test ║
╚═══════════╧═════════════════╝
REQUEST
The REQUEST
object contains a parsed and normalized request from API Gateway. It contains the following values by default:
app
: A reference to an instance of the appversion
: The version set at initializationid
: The awsRequestId from the Lambdacontext
interface
: The interface being used to access Lambda (apigateway
,alb
, oredge
)params
: Dynamic path parameters parsed from the path (see path parameters)method
: The HTTP method of the requestpath
: The path passed in by the request including thebase
and anyprefix
assigned to routesquery
: Querystring parameters parsed into an objectmultiValueQuery
: Querystring parameters with multiple values parsed into an object with array valuesheaders
: An object containing the request headers (properties converted to lowercase for HTTP/2, see rfc7540 8.1.2. HTTP Header Fields). Note that multi-value headers are concatenated with a comma per rfc2616 4.2. Message Headers.rawHeaders
: An object containing the original request headers (property case preserved)multiValueHeaders
: An object containing header values as multi-value arraysbody
: The body of the request. If theisBase64Encoded
flag istrue
, it will be decoded automatically.- If the
content-type
header isapplication/json
, it will attempt to parse the request usingJSON.parse()
- If the
content-type
header isapplication/x-www-form-urlencoded
, it will attempt to parse a URL encoded string usingquerystring
- Otherwise it will be plain text.
- If the
rawBody
: If theisBase64Encoded
flag istrue
, this is a copy of the original, base64 encoded bodyroute
: The matched route of the requestrequestContext
: TherequestContext
passed from the API GatewaypathParameters
: ThepathParameters
passed from the API GatewaystageVariables
: ThestageVariables
passed from the API GatewayisBase64Encoded
: TheisBase64Encoded
boolean passed from the API Gatewayauth
: An object containing thetype
andvalue
of an authorization header. Currently supportsBearer
,Basic
,OAuth
, andDigest
schemas. For theBasic
schema, the object is extended with additional fields for username/password. For theOAuth
schema, the object is extended with key/value pairs of the supplied OAuth 1.0 values.namespace
orns
: A reference to modules added to the app's namespace (see namespaces)cookies
: An object containing cookies sent from the browser (see the cookieRESPONSE
method)context
: Reference to thecontext
passed into the Lambda handler functioncoldStart
: Boolean that indicates whether or not the current invocation was a cold startrequestCount
: Integer representing the total number of invocations of the current function container (how many times it has been reused)ip
: The IP address of the client making the requestuserAgent
: TheUser-Agent
header sent by the client making the requestclientType
: Eitherdesktop
,mobile
,tv
,tablet
orunknown
based on CloudFront's analysis of theUser-Agent
headerclientCountry
: Two letter country code representing the origin of the requests as determined by CloudFrontstack
: An array of function names executed as part of a route's Execution Stack, which is useful for debugging
The request object can be used to pass additional information through the processing chain. For example, if you are using a piece of authentication middleware, you can add additional keys to the REQUEST
object with information about the user. See middleware for more information.
RESPONSE
The RESPONSE
object is used to send a response back to the API Gateway. The RESPONSE
object contains several methods to manipulate responses. All methods are chainable unless they trigger a response.
status(code)
The status
method allows you to set the status code that is returned to API Gateway. By default this will be set to 200
for normal requests or 500
on a thrown error. Additional built-in errors such as 404 Not Found
and 405 Method Not Allowed
may also be returned. The status()
method accepts a single integer argument.
api.get('/users', (req, res) => {
res.status(304).send('Not Modified');
});
sendStatus(code)
The sendStatus
method sets the status code and returns its string representation as the response body. The sendStatus()
method accepts a single integer argument.
res.sendStatus(200); // equivalent to res.status(200).send('OK')
res.sendStatus(304); // equivalent to res.status(304).send('Not Modified')
res.sendStatus(403); // equivalent to res.status(403).send('Forbidden')
NOTE: If an unsupported status code is provided, it will return 'Unknown' as the body.
header(key, value [,append])
The header
method allows for you to set additional headers to return to the client. By default, just the content-type
header is sent with application/json
as the value. Headers can be added or overwritten by calling the header()
method with two string arguments. The first is the name of the header and then second is the value. You can utilize multi-value headers by specifying an array with multiple values as the value
, or you can use an optional third boolean parameter and append multiple headers.
api.get('/users', (req,res) => {
res.header('content-type','text/html').send('<div>This is HTML</div>')
})
// Set multiple header values
api.get('/users', (req,res) => {
res.header('someHeader',['foo','bar').send({})
})
// Set multiple header by adding to existing header
api.get('/users', (req,res) => {
res.header('someHeader','foo')
.header('someHeader','bar',true) // append another value
.send({})
})
NOTE: Header keys are converted and stored as lowercase in compliance with rfc7540 8.1.2. HTTP Header Fields for HTTP/2. Header convenience methods (getHeader
, hasHeader
, and removeHeader
) automatically ignore case.
getHeader(key [,asArray])
Retrieve a specific header value. key
is case insensitive. By default (and for backwards compatibility), header values are returned as a string
. Multi-value headers will be concatenated using a comma (see rfc2616 4.2. Message Headers). An optional second boolean parameter can be passed to return header values as an array
.
NOTE: The ability to retrieve the current header object by calling getHeader()
is still possible, but the preferred method is to use the getHeaders()
method. By default, getHeader()
will return the object with string
values.
getHeaders()
Retrieve the current header object. Values are returned as array
s.
hasHeader(key)
Returns a boolean indicating the existence of key
in the response headers. key
is case insensitive.
removeHeader(key)
Removes header matching key
from the response headers. key
is case insensitive. This method is chainable.
getLink(s3Path [, expires] [, callback])
This returns a signed URL to the referenced file in S3 (using the s3://{my-bucket}/{path-to-file}
format). You can optionally pass in an integer as the second parameter that will changed the default expiration time of the link. The expiration time is in seconds and defaults to 900
. In order to ensure proper URL signing, the getLink()
must be asynchronous, and therefore returns a promise. You must either await
the result or use a .then
to retrieve the value.
There is an optional third parameter that takes an error handler callback. If the underlying getSignedUrl()
call fails, the error will be returned using the standard res.error()
method. You can override this by providing your own callback.
// async/await
api.get('/getLink', async (req, res) => {
let url = await res.getLink('s3://my-bucket/my-file.pdf');
return { link: url };
});
// promises
api.get('/getLink', (req, res) => {
res.getLink('s3://my-bucket/my-file.pdf').then((url) => {
res.json({ link: url });
});
});
send(body)
The send
methods triggers the API to return data to the API Gateway. The send
method accepts one parameter and sends the contents through as is, e.g. as an object, string, integer, etc. AWS Gateway expects a string, so the data should be converted accordingly.
json(body)
There is a json
convenience method for the send
method that will set the headers to application/json
as well as perform JSON.stringify()
on the contents passed to it.
api.get('/users', (req, res) => {
res.json({ message: 'This will be converted automatically' });
});
jsonp(body)
There is a jsonp
convenience method for the send
method that will set the headers to application/json
, perform JSON.stringify()
on the contents passed to it, and wrap the results in a callback function. By default, the callback function is named callback
.
res.jsonp({ foo: 'bar' });
// => callback({ "foo": "bar" })
res.status(500).jsonp({ error: 'some error' });
// => callback({ "error": "some error" })
The default can be changed by passing in callback
as a URL parameter, e.g. ?callback=foo
.
// ?callback=foo
res.jsonp({ foo: 'bar' });
// => foo({ "foo": "bar" })
You can change the default URL parameter using the optional callback
option when initializing the API.
const api = require('lambda-api')({ callback: 'cb' });
// ?cb=bar
res.jsonp({ foo: 'bar' });
// => bar({ "foo": "bar" })
html(body)
There is also an html
convenience method for the send
method that will set the headers to text/html
and pass through the contents.
api.get('/users', (req, res) => {
res.html('<div>This is HTML</div>');
});
type(type)
Sets the content-type
header for you based on a single String
input. There are thousands of MIME types, many of which are likely never to be used by your application. Lambda API stores a list of the most popular file types and will automatically set the correct content-type
based on the input. If the type
contains the "/" character, then it sets the content-type
to the value of type
.
res.type('.html'); // => 'text/html'
res.type('html'); // => 'text/html'
res.type('json'); // => 'application/json'
res.type('application/json'); // => 'application/json'
res.type('png'); // => 'image/png'
res.type('.doc'); // => 'application/msword'
res.type('text/css'); // => 'text/css'
For a complete list of auto supported types, see mimemap.js. Custom MIME types can be added by using the mimeTypes
option when instantiating Lambda API
location(path)
The location
convenience method sets the Location:
header with the value of a single string argument. The value passed in is not validated but will be encoded before being added to the header. Values that are already encoded can be safely passed in. Note that a valid 3xx
status code must be set to trigger browser redirection. The value can be a relative/absolute path OR a FQDN.
api.get('/redirectToHome', (req, res) => {
res.location('/home').status(302).html('<div>Redirect to Home</div>');
});
api.get('/redirectToGithub', (req, res) => {
res
.location('https://github.com')
.status(302)
.html('<div>Redirect to GitHub</div>');
});
redirect([status,] path)
The redirect
convenience method triggers a redirection and ends the current API execution. This method is similar to the location()
method, but it automatically sets the status code and calls send()
. The redirection URL (relative/absolute path, a FQDN, or an S3 path reference) can be specified as the only parameter or as a second parameter when a valid 3xx
status code is supplied as the first parameter. The status code is set to 302
by default, but can be changed to 300
, 301
, 302
, 303
, 307
, or 308
by adding it as the first parameter.
api.get('/redirectToHome', (req, res) => {
res.redirect('/home');
});
api.get('/redirectToGithub', (req, res) => {
res.redirect(301, 'https://github.com');
});
// This will redirect a signed URL using the getLink method
api.get('/redirectToS3File', (req, res) => {
res.redirect('s3://my-bucket/someFile.pdf');
});
cors([options])
Convenience method for adding CORS headers to responses. An optional options
object can be passed in to customize the defaults.
The six defined CORS headers are as follows:
- Access-Control-Allow-Origin (defaults to
*
) - Access-Control-Allow-Methods (defaults to
GET, PUT, POST, DELETE, OPTIONS
) - Access-Control-Allow-Headers (defaults to
Content-Type, Authorization, Content-Length, X-Requested-With
) - Access-Control-Expose-Headers
- Access-Control-Max-Age
- Access-Control-Allow-Credentials
The options
object can contain the following properties that correspond to the above headers:
- origin (string)
- methods (string)
- headers (string)
- exposeHeaders (string)
- maxAge (number in milliseconds)
- credentials (boolean)
Defaults can be set by calling res.cors()
with no properties, or with any combination of the above options.
res.cors({
origin: 'example.com',
methods: 'GET, POST, OPTIONS',
headers: 'content-type, authorization',
maxAge: 84000000,
});
You can override existing values by calling res.cors()
with just the updated values:
res.cors({
origin: 'api.example.com',
});
error([code], message [,detail])
An error can be triggered by calling the error
method. This will cause the API to stop execution and return the message to the client. The status code can be set by optionally passing in an integer as the first parameter. Additional detail can be added as an optional third parameter (or second parameter if no status code is passed). This will add an additional detail
property to error logs. Details accepts any value that can be serialized by JSON.stringify
including objects, strings and arrays. Custom error handling can be accomplished using the Error Handling feature.
api.get('/users', (req, res) => {
res.error('This is an error');
});
api.get('/users', (req, res) => {
res.error(403, 'Not authorized');
});
api.get('/users', (req, res) => {
res.error('Error', { foo: 'bar' });
});
api.get('/users', (req, res) => {
res.error(404, 'Page not found', 'foo bar');
});
cookie(name, value [,options])
Convenience method for setting cookies. This method accepts a name
, value
and an optional options
object with the following parameters:
| Property | Type | Description |
| -------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| domain | String
| Domain name to use for the cookie. This defaults to the current domain. |
| expires | Date
| The expiration date of the cookie. Local dates will be converted to GMT. Creates session cookie if this value is not specified. |
| httpOnly | Boolean
| Sets the cookie to be accessible only via a web server, not JavaScript. |
| maxAge | Number
| Set the expiration time relative to the current time in milliseconds. Automatically sets the expires
property if not explicitly provided. |
| path | String
| Path for the cookie. Defaults to "/" for the root directory. |
| secure | Boolean
| Sets the cookie to be used with HTTPS only. |
| sameSite | Boolean
or String
| Sets the SameSite value for cookie. true
or false
sets Strict
or Lax
respectively. Also allows a string value. See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1 |
The name
attribute should be a string (auto-converted if not), but the value
attribute can be any type of value. The value
will be serialized (if an object, array, etc.) and then encoded using encodeURIComponent
for safely assigning the cookie value. Cookies are automatically parsed, decoded, and available via the REQUEST
object (see REQUEST).
NOTE: The cookie()
method only sets the header. A execution ending method like send()
, json()
, etc. must be called to send the response.
res.cookie('foo', 'bar', { maxAge: 3600 * 1000, secure: true }).send();
res
.cookie(
'fooObject',
{ foo: 'bar' },
{ domain: '.test.com', path: '/admin', httpOnly: true }
)
.send();
res
.cookie('fooArray', ['one', 'two', 'three'], { path: '/', httpOnly: true })
.send();
clearCookie(name [,options])
Convenience method for expiring cookies. Requires the name
and optional options
object as specified in the cookie method. This method will automatically set the expiration time. However, most browsers require the same options to clear a cookie as was used to set it. E.g. if you set the path
to "/admin" when you set the cookie, you must use this same value to clear it.
res.clearCookie('foo', { secure: true }).send();
res
.clearCookie('fooObject', {
domain: '.test.com',
path: '/admin',
httpOnly: true,
})
.send();
res.clearCookie('fooArray', { path: '/', httpOnly: true }).send();
NOTE: The clearCookie()
method only sets the header. A execution ending method like send()
, json()
, etc. must be called to send the response.
etag([boolean])
Enables Etag generation for the response if at value of true
is passed in. Lambda API will generate an Etag based on the body of the response and return the appropriate header. If the request contains an If-No-Match
header that matches the generated Etag, a 304 Not Modified
response will be returned with a blank body.
cache([age] [, private])
Adds cache-control
header to responses. If the first parameter is an integer
, it will add a max-age
to the header. The number should be in milliseconds. If the first parameter is true
, it will add the cache headers with max-age
set to 0
and use the current time for the expires
header. If set to false, it will add a cache header with no-cache, no-store, must-revalidate
as the value. You can also provide a custom string that will manually set the value of the cache-control
header. And optional second argument takes a boolean
and will set the cache-control
to private
This method is chainable.
res.cache(false).send(); // 'cache-control': 'no-cache, no-store, must-revalidate'
res.cache(1000).send(); // 'cache-control': 'max-age=1'
res.cache(30000, true).send(); // 'cache-control': 'private, max-age=30'
modified(date)
Adds a last-modified
header to responses. A value of true
will set the value to the current date and time. A JavaScript Date
object can also be passed in. Note that it will be converted to UTC if not already. A string
can also be passed in and will be converted to a date if JavaScript's Date()
function is able to parse it. A value of false
will prevent the header from being generated, but will not remove any existing last-modified
headers.
attachment([filename])
Sets the HTTP response content-disposition
header field to "attachment". If a filename
is provided, then the content-type
is set based on the file extension using the type()
method and the "filename=" parameter is added to the content-disposition
header.
res.attachment();
// content-disposition: attachment
res.attachment('path/to/logo.png');
// content-disposition: attachment; filename="logo.png"
// content-type: image/png
download(file [, filename] [, options] [, callback])
This transfers the file
(either a local path, S3 file reference, or Javascript Buffer
) as an "attachment". This is a convenience method that combines attachment()
and sendFile()
to prompt the user to download the file. This method optionally takes a filename
as a second parameter that will overwrite the "filename=" parameter of the content-disposition
header, otherwise it will use the filename from the file
. An optional options
object passes through to the sendFile() method and takes the same parameters. Finally, a optional callback
method can be defined which is passed through to sendFile() as well.
res.download('./files/sales-report.pdf')
res.download('./files/sales-report.pdf', 'report.pdf')
res.download('s3://my-bucket/path/to/file.png', 'logo.png', { maxAge: 3600000 })
res.download(<Buffer>, 'my-file.docx', { maxAge: 3600000 }, (err) => {
if (err) {
res.error('Custom File Error')
}
})
sendFile(file [, options] [, callback])
The sendFile()
method takes up to three arguments. The first is the file
. This is either a local filename (stored within your uploaded lambda code), a reference to a file in S3 (using the s3://{my-bucket}/{path-to-file}
format), or a JavaScript Buffer
. You can optionally pass an options
object using the properties below as well as a callback function callback(err)
that can handle custom errors or manipulate the response before sending to the client.
| Property | Type | Description | Default |
| ------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| maxAge | Number
| Set the expiration time relative to the current time in milliseconds. Automatically sets the Expires
header | 0 |
| root | String
| Root directory for relative filenames. | |
| lastModified | Boolean
or String
| Sets the last-modified
header to the last modified date of the file. This can be disabled by setting it to false
, or overridden by setting it to a valid Date
object | |
| headers | Object
| Key value pairs of additional headers to be sent with the file | |
| cacheControl | Boolean
or String
| Enable or disable setting cache-control
response header. Override value with custom string. | true |
| private | Boolean
| Sets the cache-control
to private
. | false |
res.sendFile('./img/logo.png')
res.sendFile('./img/logo.png', { maxAge: 3600000 })
res.sendFile('s3://my-bucket/path/to/file.png', { maxAge: 3600000 })
res.sendFile(<Buffer>, 'my-file.docx', { maxAge: 3600000 }, (err) => {
if (err) {
res.error('Custom File Error')
}
})
The callback
function supports returning a promise, allowing you to perform additional tasks after the file is successfully loaded from the source. This can be used to perform additional synchronous tasks before returning control to the API execution.
NOTE: In order to access S3 files, your Lambda function must have GetObject
access to the files you're attempting to access.
See Enabling Binary Support for more information.
Enabling Binary Support
To enable binary support, you need to add */*
under "Binary Media Types" in API Gateway -> APIs -> [ your api ] -> Settings. This will also base64
encode all body content, but Lambda API will automatically decode it for you.
Add */*
to Binary Media Types
Path Parameters
Path parameters are extracted from the path sent in by API Gateway. Although API Gateway supports path parameters, the API doesn't use these values but insteads extracts them from the actual path. This gives you more flexibility with the API Gateway configuration. Path parameters are defined in routes using a colon :
as a prefix.
api.get('/users/:userId', (req, res) => {
res.send('User ID: ' + req.params.userId);
});
Path parameters act as wildcards that capture the value into the params
object. The example above would match /users/123
and /users/test
. The system always looks for static paths first, so if you defined paths for /users/test
and /users/:userId
, exact path matches would take precedence. Path parameters only match the part of the path they are defined on. E.g. /users/456/test
would not match /users/:userId
. You would either need to define /users/:userId/test
as its own path, or create another path with an additional path parameter, e.g. /users/:userId/:anotherParam
.
A path can contain as many parameters as you want. E.g. /users/:param1/:param2/:param3
.
Wildcard Routes
Wildcard routes are supported for matching arbitrary paths. Wildcards only work at the end of a route definition such as /*
or /users/*
. Wildcards within a path, e.g. /users/*/posts
are not supported. Wildcard routes do support parameters, however, so /users/:id/*
would capture the :id
parameter in your wildcard handler.
Wildcard routes will match any deep paths after the wildcard. For example, a GET
method for path /users/*
would match /users/1/posts/latest
. The only exception is for the OPTIONS
method. A path must exist for a wildcard on an OPTIONS
route in order to execute the handler. If a wildcard route is defined for another method higher up the path, then the OPTIONS
handler will fire. For example, if there was a POST
method defined on /users/*
, then an OPTIONS
method for /users/2/posts/*
would fire as it assumes that the POST
path would exist.
In most cases, Path Parameters should be used in favor of wildcard routes. However, if you need to support unpredictable path lengths, or your are building single purpose functions and will be mapping routes from API Gateway, the wildcards are a powerful pattern. Another good use case is to use the OPTIONS
method to provide CORS headers.
api.options('/*', (req, res) => {
// Return CORS headers
res.cors().send({});
});
Logging
Lambda API includes a robust logging engine specifically designed to utilize native JSON support for CloudWatch Logs. Not only is it ridiculously fast, but it's also highly configurable. Logging is disabled by default, but can be enabled by passing { logger: true }
when you create the Lambda API instance (or by passing a Logging Configuration definition).
The logger is attached to the REQUEST
object and can be used anywhere the object is available (e.g. routes, middleware, and error handlers).
const api = require('lambda-api')({ logger: true });
api.get('/status', (req, res) => {
req.log.info('Some info about this route');
res.send({ status: 'ok' });
});
In addition to manual logging, Lambda API can also generate "access" logs for your API requests. API Gateway can also provide access logs, but they are limited to contextual information about your request (see here). Lambda API allows you to capture the same data PLUS additional information directly from within your application.
Logging Configuration
Logging can be enabled by setting the logger
option to true
when creating the Lambda API instance. Logging can be configured by setting logger
to an object that contains configuration information. The following table contains available logging configuration properties.
| Property | Type | Description | Default |
| ------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
| access | boolean
or string
| Enables/disables automatic access log generation for each request. See Access Logs. | false
|
| errorLogging | boolean
| Enables/disables automatic error logging. | true
|
| customKey | string
| Sets the JSON property name for custom data passed to logs. | custom
|
| detail | boolean
| Enables/disables adding REQUEST
and RESPONSE
data to all log entries. | false
|
| level | string
| Minimum logging level to send logs for. See Logging Levels. | info
|
| levels | object
| Key/value pairs of custom log levels and their priority. See Custom Logging Levels. | |
| log | function
| Custom function for overriding standard console.log
. | console.log
|
| messageKey | string
| Sets the JSON property name of the log "message". | msg
|
| multiValue | boolean
| Enables multi-value support for querystrings. If enabled, the qs
parameter will return all values as array
s and will include multiple values if they exist. | false
|
| nested | boolean
| Enables/disables nesting of JSON logs for serializer data. See Serializers. | false
|
| timestamp | boolean
or function
| By default, timestamps will return the epoch time in milliseconds. A value of false
disables log timestamps. A function that returns a value can be used to override the default format. | true
|
| sampling | object
| Enables log sampling for periodic request tracing. See Sampling. | |
| serializers | object
| Adds serializers that manipulate the log format. See Serializers. | |
| stack | boolean
| Enables/disables the inclusion of stack traces in caught errors. | false
|
Example:
const api = require('lambda-api')({
logger: {
level: 'debug',
access: true,
customKey: 'detail',
messageKey: 'message',
timestamp: () => new Date().toUTCString(), // custom timestamp
stack: true,
},
});
Log Format
Logs are generated using Lambda API's standard JSON format. The log format can be customized using Serializers.
Standard log format (manual logging):
{
"level": "info", // log level
"time": 1534724904910, // request timestamp
"id": "41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9", // awsRequestId
"route": "/user/:userId", // route accessed
"method": "GET", // request method
"msg": "Some info about this route", // log message
"timer": 2, // execution time up until log was generated
"custom": "additional data", // addditional custom log detail
"remaining": 2000, // remaining milliseconds until function timeout
"function": "my-function-v1", // function name
"memory": 2048, // allocated function memory
"int": "apigateway", // interface used to access the Lambda function
"sample": true // is generated during sampling request?
}
Access Logs
Access logs generate detailed information about the API request. Access logs are disabled by default, but can be enabled by setting the access
property to true
in the logging configuration object. If set to false
, access logs will only be generated when other log entries (info
, error
, etc.) are created. If set to the string 'never'
, access logs will never be generated.
Access logs use the same format as the standard logs above, but include additional information about the request. The access log format can be customized using Serializers.
Access log format (automatic logging):
{
... Standard Log Data ...,
"path": "/user/123", // path accessed
"ip": "12.34.56.78", // client ip address
"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)...", // User-Agent
"version": "v1", // specified API version
"device": "mobile", // client device (as determined by CloudFront)
"country": "US", // client country (as determined by CloudFront)
"qs": { // query string parameters
"foo": "bar"
}
}
Logging Levels
Logging "levels" allow you to add detailed logging to your functions based on severity. There are six standard log levels as specified in the table below along with their default priority.
| Level | Priority |
| ------- | -------- |
| trace
| 10 |
| debug
| 20 |
| info
| 30 |
| warn
| 40 |
| error
| 50 |
| fatal
| 60 |
Logs are written to CloudWatch Logs ONLY if they are the same or higher severity than specified in the level
log configuration.
// Logging level set to "warn"
const api = require('lambda-api')({ logger: { level: 'warn' } });
api.get('/', (req, res) => {
req.log.trace('trace log message'); // ignored
req.log.debug('debug log message'); // ignored
req.log.info('info log message'); // ignored
req.log.warn('warn log message'); // write to CloudWatch
req.log.error('error log message'); // write to CloudWatch
req.log.fatal('fatal log message'); // write to CloudWatch
res.send({ hello: 'world' });
});
Custom Logging Levels
Custom logging "levels" can be added by specifying an object containing "level names" as keys and their priorities as values. You can also adjust the priority of standard levels by adding it to the object.
const api = require('lambda-api')({
logger: {
levels: {
test: 5, // low priority 'test' level
customLevel: 35, // between info and warn
trace: 70, // set trace to the highest priority
},
},
});
In the example above, the test
level would only generate logs if the priority was set to test
. customLevel
would generate logs if level
was set to anything with the same or lower priority (e.g. info
). trace
now has the highest priority and would generate a log entry no matter what the level was set to.
Adding Additional Detail
Manual logging also allows you to specify additional detail with each log entry. Details can be added by suppling any variable type as a second parameter to the logger function.
req.log.info('This is the main log message', 'some other detail'); // string
req.log.info('This is the main log message', {
foo: 'bar',
isAuthorized: someVar,
}); // object
req.log.info('This is the main log message', 25); // number
req.log.info('This is the main log message', ['val1', 'val2', 'val3']); // array
req.log.info('This is the main log message', true); // boolean
If an object
is provided, the keys will be merged into the main log entry's JSON. If any other type
is provided, the value will be assigned to a key using the the customKey
setting as its property name. If nested
is set to true
, objects will be nested under the value of customKey
as well.
Serializers
Serializers allow you to customize log formats as well as add additional data from your application. Serializers can be defined by adding a serializers
property to the logger
configuration object. A property named for an available serializer (main
, req
, res
, context
or custom
) needs to return an anonymous function that takes one argument and returns an object. The returned object will be merged into the main JSON log entry. Existing properties can be removed by returning undefined
as their values.
const api = require('lambda-api')({
logger: {
serializers: {
req: (req) => {
return {
apiId: req.requestContext.apiId, // add the apiId
stage: req.requestContext.stage, // add the stage
qs: undefined, // remove the query string
};
},
},
},
});
Serializers are passed one argument that contains their corresponding object. req
and main
receive the REQUEST
object, res
receives the RESPONSE
object, context
receives the context
object passed into the main run
function, and custom
receives custom data passed in to the logging methods. Note that only custom objects
will trigger the custom
serializer.
If the nested
option is set to true in the logger
configuration, then JSON log entries will be generated with properties for req
, res
, context
and custom
with their serialized data as nested objects.
Sampling
Sampling allows you to periodically generate log entries for all possible severities within a single request execution. All of the log entries will be written to CloudWatch Logs and can be used to trace an entire request. This can be used for debugging, metric samples, resource response time sampling, etc.
Sampling can be enabled by adding a sampling
property to the logger
configuration object. A value of true
will enable the default sampling rule. The default can be changed by passing in a configuration object with the following available optional properties:
| Property | Type | Description | Default |
| -------- | -------- | ------------------------------------------------------------------------ | ------- |
| target | number
| The minimum number of samples per period
. | 1 |
| rate | number
| The percentage of samples to be taken during the period
. | 0.1 |
| period | number
| Number of seconds representing the duration of each sampling period. | 60 |
The example below would sample at least 2
requests every 30
seconds as well as an additional 0.1
(10%) of all other requests during that period. Lambda API tracks the velocity of requests and attempts to distribute the samples as evenly as possible across the specified period
.
const api = require('lambda-api')({
logger: {
sampling: {
target: 2,
rate: 0.1,
period: 30,
},
},
});
Additional rules can be added by specifying a rules
parameter in the sampling
configuration object. The rules
should contain an array
of "rule" objects with the following properties:
| Property | Type | Description | Default | Required |
| -------- | ------------------- | ------------------------------------------------------------------------ | ------- | -------- |
| route | string
| The route (as defined in a route handler) to apply this rule to. | | Yes |
| target | number
| The minimum number of samples per period
. | 1 | No |
| rate | number
| The percentage of samples to be taken during the period
. | 0.1 | No |
| period | number
| Number of seconds representing the duration of each sampling period. | 60 | No |
| method | string
or array
| A comma separated list or array
of HTTP methods to apply this rule to. | | No |
The route
property is the only value required and must match a route's path definition (e.g. /user/:userId
, not /user/123
) to be activated. Routes can also use wildcards at the end of the route to match multiple routes (e.g. /user/*
would match /user/:userId
AND /user/:userId/tags
). A list of method
s can also be supplied that would limit the rule to just those HTTP methods. A comma separated string
or an array
will be properly parsed.
Sampling rules can be used to disable sampling on certain routes by setting the target
and rate
to 0
. For example, if you had a /status
route that you didn't want to be sampled, you would use the following configuration:
const api = require('lambda-api')({
logger: {
sampling: {
rules: [{ route: '/status', target: 0, rate: 0 }],
},
},
});
You could also use sampling rules to enable sampling on certain routes:
const api = require('lambda-api')({
logger: {
sampling: {
rules: [
{ route: '/user', target: 1, rate: 0.1 }, // enable for /user route
{ route: '/posts/*', target: 1, rate: 0.1 }, // enable for all routes that start with /posts
],
target: 0, // disable sampling default target
rate: 0, // disable sampling default rate
},
},
});
If you'd like to disable sampling for GET
and POST
requests to user:
const api = require('lambda-api')({
logger: {
sampling: {
rules: [
// disable GET and POST on /user route
{ route: '/user', target: 0, rate: 0, method: ['GET', 'POST'] },
],
},
},
});
Any combination of rules can be provided to customize sampling behavior. Note that each rule tracks requests and velocity separately, which could limit the number of samples for infrequently accessed routes.
Middleware
The API supports middleware to preprocess requests before they execute their matching routes. Global middleware is defined using the use
method one or more functions with three parameters for the REQUEST
, RESPONSE
, and next
callback. For example:
api.use((req, res, next) => {
// do something
next();
});
Middleware can be used to authenticate requests, create database connections, etc. The REQUEST
and RESPONSE
objects behave as they do within routes, allowing you to manipulate either obj