typescript-lambda-api
Build REST API's using Typescript & AWS Lambda.
Read the full typedoc
documentation: https://djfdyuruiry.github.io/typescript-lambda-api/
Framework Features:
- Decorator based routing for API controllers and endpoint methods
- Decorator based parameter binding for endpoint methods (from body, path & query parameters and headers)
- Built in support for applying JSON patch operations
- API controller dependency injection using InversifyJS
- Supports invoking your API from both
Amazon API Gateway
andAmazon Load Balancer
This project is built on top of the wonderful lambda-api framework.
Quickstart
Docs
Creating a new API
This is a short guide to creating your first API using typescript-lambda-api
. It is somewhat opinionated about project structure, but most of this can be easily customised.
Note: Node.js v8 & Typescript v3 or newer are required to use this package.
-
Create a directory for your project and run
npm init
to create yourpackage.json
-
Install required packages:
Ensure the @types/node
package you install matches your version of Node.js
npm install typescript-lambda-apinpm install -D typescript @types/node aws-sdk
- Open
package.json
and add a script to enable access to the Typescript compiler:
- Create a new file named
tsconfig.json
, add the following:
Note: emitDecoratorMetadata
, experimentalDecorators
and strict
flags are required to be set as shown above to compile your app
-
Create a new directory named
src
-
Create a new file named
src/api.ts
, add the following:
appConfig.base = "/api/v1"appConfig.version = "v1"
-
Add a
src/controllers
directory -
Create a new file in
controllers
namedHelloWorldController.ts
, add the following:
// all controller classes must be decorated with injectable// extending Controller is optional, it provides convience methods
- Compile the application by running:
npm run tsc
Deploy to AWS Lambda
Note: AWS supplies the aws-sdk
package at runtime when running your Lambda applications, so there is no need to include this in your deployment package.
-
Build your application
-
Remove dev dependencies from your
node_modules
directory:
rm -rf node_modules
npm install --only=prod
This will massively reduce the size of your deployment package
- Run the following commands to package your app:
zip -r dist/lambda.zip node_modulescd distzip -r lambda.zip ./
- Upload your lambda using the
dist/lambda.zip
file. Specifyapp.handler
as the function handler. See: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-create-deployment-pkg.html
Invoke Lambda
-
Create an AWS Load Balancer and point it to your new API Lambda. See: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html
-
You can now call your new ALB to see your API in action:
wget -qO - https://some.alb.dns.address/api/v1/hello-world/
- You should see:
Routing
Routing is configured using decorators on both controller classes and endpoint methods. You can also define a global base path (e.x. /api/v1
) for your API by configuring the base
property when passing your app configuration to the ApiLambdaApp
class. (See the Creating a new API
section)
Controller Routes
You can declare a root path for all methods in a controller using the apiController
decorator.
Endpoint Routes
You can declare a path for any given method in a controller when using the endpoint decorators. The apiController
decorator is not required on the class to use this form of routing.
Path Parameters
You can include parameters as part of your routes, when you need to capture parts of the URL.
You can also combine controller and endpoint path parameters.
Note all path parameters are passed in as strings, you will need to cast these if required
Request Parameter Binding
Different parts of the HTTP request can be bound to endpoint method parameters using decorators.
queryParam
- Query string parameterheader
- HTTP header valuefromBody
- Entity from request body, this will be an object if the request contains JSON, otherwise it will simply be a string
Responses
There are two ways to respond to requests:
- Return a value from your endpoint method
- Use the response context to send a response (see
Request / Response Context
section below - the context has convience methods for html, json etc.)
By default all return values are serialised to JSON in the response body and the content-type
response header is set to application/json
. To change this you can use the produces
and controllerProduces
decorators.
Only JSON content types are serialised automatically, all other types simply convert the return value to a string.
To set the response content type for all methods, use controllerProduces
on a class.
For an individual method, use produces
. This will override controllerProduces
for that method, if present on the controller class.
Authentication & Authorization
This framework supports authenticating requests and authorization for controllers and endpoints. It can be used to configure HTTP authentication, token based auth and role based access control (ACLs).
Implementation is heavily inspired by the Dropwizard framework for Java.
Authentication and Principals
Authentication is preformed by filter classes that are executed before invoking an endpoint; all filter classes implement the IAuthFilter
interface.
Filters use information from the HTTP request to authenticate the request. If authentication is successful, a filter will return a principal. A principal is a simple class that contains information about the current user/entity that has been granted access to the endpoint.
To use authentication you must implement your own principal by extending the Principal
class:
Basic Authentication
HTTP Basic authentication is supported out of the box by the BasicAuthFilter
filter abstract class. You extend this class to implement your authentication logic:
You register your authentication filter when setting up your application instance:
// build config and controllers path... // this will protect your endpoints using the auth filter to authenticate requestsapp.middlewareRegistry.addAuthFilterauthFilter// export handler
Access Principal Context
Once a user has been authenticated you can pass the principal instance into the target endpoint. You can do this by adding a principal
parameter decorator to your endpoint method.
Unauthenticated Endpoints
There are several situations where you might want to disable authentication for a specific endpoint:
- Healthcheck / Status endpoint
- Login Endpoint
- Public API endpoints for unauthenticated users (browsing products without logging in)
To do this you need to use the noAuth
and controllerNoAuth
decorators.
For an endpoint:
For all endpoints in a controller:
Custom Authentication
If you wish to implement popular authentication mechnasims or make your own, you need to implement the IAuthFilter
interface. It accepts two type parameters:
T
- The model class for your authentication dataU
- A principal class
Authentication data classes are free form, for example:
Your auth filter implementation must provide a method for extracting your authentication data, and a method that uses that data to authenticate the current request.
Tip: You can make your class abstract and then make the authenticate
method abstract to enable your custom auth filter to be re-usable. This way, you simply extend your custom fiter and implement the authentication logic for your application.
Authorization
To implement role based authorization you implement the IAuthorizer
interface.
When a user is successfully authorized by an auth filter, this returns a principal which is passed to the configured authorizer if a resource is marked as restricted. To restrict all endpoints in a controller, use the controllerRolesAllowed
decorator:
You can restrict a single enpoint using the rolesAllowed
decorator:
You can combine both ther controller and endpoint decorators for roles. In this case, if endpoint roles are present, they overrides the controller roles.
You register your authentication filter when setting up your application instance:
// build config and controllers path... // this will protect your endpoints using the authorizer to check access rolesapp.middlewareRegistry.addAuthorizerauthorizer// export handler
Error Handling
When an unexpected error is thrown in one of your endpoints, you can choose how to handle this. There are three general techniques:
- Use an error interceptor
- Catch the error in your endpoint logic
- Let the framework handle the error
Error Interceptors
Error interceptors are classes that can be configured to be invoked when an error occurs when calling a given controller or endpoint. Interceptors extend the ErrorInterceptor
class and provide an implementation for an intercept
method.
Interceptor instances are built using the InversifyJS app container, so you can add any dependencies as constructor parameters if you configure the container correctly.
;
In your controller you can then use the controllerErrorInterceptor
decorator to specify the error interceptor to use:
You can also use the errorInterceptor
decorator on individual endpoints for more fine grained error control. Endpoint interceptors will override controller interceptors.
Manual Error Interceptors
You can manually register interceptors when setting up your application instance:
// build config and controllers path... // this will intercept errors thrown by any endpointapp.middlewareRegistry.addErrorInterceptorerrorInterceptor// export handler
You can intercept only the errors thrown by an endpoint by setting endpointTarget
:
// pattern for endpoints is {controller class name}::{endpoint method name}errorInterceptor.endpointTarget = "StoreController::getItems"
You can intercept only the errors thrown by a controller by setting controllerTarget
:
// controllers are identified by class nameerrorInterceptor.controllerTarget = "StoreController"
Note: using this type of interceptor is overridden if the target controller or endpoint has an interceptor configured
Catching Errors
You can use a try/catch block and the Response
class to handle errors:
Note: this can also be done by injecting the Response
class instance using the response
parameter decorator, instead of extending Controller
.
Framework Error Handling
If you simply preform your logic in your endpoint method without catching any errors yourself, the framework will catch the error and return a HTTP 500 response with error details. Below is a JSON snippet showing an example.
JSON Patch Requests
This library supports JSON Patch format for updating entities without having to upload the entire entity. To use it in your endpoints, ensure your controller extends the Controller
class, an example is below:
Under the hood, the API uses the fast-json-patch package
Request / Response Context
If you want to read request bodies or write to the response, there are several supported approaches.
Extending Controller Class
If you extend the controller class, you get access to the request and response context.
Using Decorators
You can use parameter decorators to inject the request and response context.
The Request
and Response
classes are documented in the lambda-api package.
Dependency Injection
Configuring the IOC container to enable dependency injection into your controllers is easy. Once you build a ApiLambdaApp
instance you can call the configureApp
method like below:
// build config and controllers path... app.configureApp // export handler
Note: Any classes that you are going to inject need to be decorated with injectable
, any subclasses are also required to be decorated
In your controllers you can then use the registered types as constructor parameters:
See the InversifyJS package documentation for full guidance how to use the Container
class to manage dependencies.
Configuration
When building an application instance you pass a AppConfig
instance to the constructor. If you want to provide your own application config it is recommended to extend this class .
You can then configure the IOC container to bind to your configuration instance.
// build controllers path... app.configureApp // export handler
After which, you can inject your config into your controllers or services.
Note: The AppConfig
class supports all the configuration fields documented in the lambda-api package.
Reference
For a complete reference see the AppConfig docs.
lambda-api
Configuring lambda-api
directly can be done by calling the configureApi
method like below:
// build config and controllers path... app.configureApiapi: // export handler
Note: any middleware handlers and manual routes will not apply auth filters, authorizers or error interceptors
See the lambda-api package documentation for guidance how to use the API
class.
Logging
A logger interface is provided that can write messages to standard out. You can configure this logger using the serverLogging
key in the AppConfig
class. See the Config Reference for details on options available.
By default, the logger is set to info
and outputs messages as simple strings.
The format of the messages written out is:
ISO 8601 Datetime level class message
vvvvvvvvvvvvvvvvvvvvvvvv vvvv vvvvvvvv vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
2019-04-21T16:38:09.680Z INFO Endpoint - Invoking endpoint: [GET] /open-api.yml
Below is some example output, include a stack trace from an Error
instance:
2019-04-21T22:20:29.622Z INFO ApiLambdaApp - Received event, initialising controllers and processing event
2019-04-21T22:20:29.647Z INFO Server - Processing API request event for path: /test/
2019-04-21T22:20:29.832Z INFO Endpoint - [GET] /test - Authenticating request
2019-04-21T22:20:29.833Z ERROR Endpoint - [GET] /test - Error processing endpoint request
Error: authenticate failed
at TestAuthFilter.authenticate (/home/matthew/src/ts/typescript-lambda-api/tests/src/test-components/TestAuthFilter.ts:25:19)
at Endpoint.authenticateRequest (/home/matthew/src/ts/typescript-lambda-api/dist/api/Endpoint.js:15:2640)
at processTicksAndRejections (internal/process/task_queues.js:86:5)
at process.runNextTicks [as _tickCallback] (internal/process/task_queues.js:56:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:880:11)
at runMain (/home/matthew/.node-spawn-wrap-13541-13c0098ec456/node:68:10)
at Function.<anonymous> (/home/matthew/.node-spawn-wrap-13541-13c0098ec456/node:171:5)
at Object.<anonymous> (/home/matthew/src/ts/typescript-lambda-api/node_modules/nyc/bin/wrap.js:23:4)
at Module._compile (internal/modules/cjs/loader.js:816:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:827:10)
If you set the format
to json
the log messages will look like this:
This format matches the keys used by the lambda-api
framework in it's output.
Writing Logs
To write logs you will ned a logger instance. There are three ways to get one:
- Extend the
Controller
class in your controller:
- Use a
LogFactory
instance to build it:
- Use the
LogFactory
static methods to build it:
Logger API
The logging API supports formatting of messages using the sprintf-js
npm module, simply pass in your arguments and put placeholders in your message string:
logger.warn"Hello there %s, how are you?", "Roy"logger.debug"Task status: %s. Task data: %j", "success",
Using this will help to speed up your app if you do a lot of logging, because uneccessary work to convert values to strings and JSON for debug messages will not take place if a higher error level is set.
Below is an example of the methods available on logger instances:
lambda-api
Logging is also provided by the lambda-api package, use the AppConfig
instance passed to ApiLambdaApp
to configure logging using the logger
key. See the Config Reference for details on options available.
OpenAPI (Swagger)
The OpenAPI Specification, FKA Swagger, is supported out of the box. If you are not familar with it, check out https://github.com/OAI/OpenAPI-Specification
This framework only supports OpenAPI v3
The following features are supported:
- Generating of an OpenAPI Specification, which includes:
- All endpoints with full path and HTTP method
- Custom names and descriptions for endpoints
- Group endpoints together by API
- Endpoint query, path and header parameters (set by parameter decorators)
- Response content type headers (set by
produces
orcontrollerProduces
decorators) - Request and Response bodies: class types, primitive values and files
- Response HTTP status codes
- HTTP Basic security scheme (when a basic auth filter is configured)
- Custom auth filter security schemes
- Specification files can be generated in
JSON
orYAML
format (see YAML Support)
To enable it, use the openApi
property in the AppConfig
class when building your app:
// build controllers path... appConfig.base = "/api/v1"appConfig.version = "v1"appConfig.openApi.enabled = true // export handler
You can then request your specification using the paths:
/api/v1/open-api.json
- JSON format/api/v1/open-api.yml
- YAML format
Decorators
To further document your API endpoints you can use OpenAPI decorators.
-
Customize the names of APIs and endpoints using
api
:// the second parameter is optional
The same @api
name can be used on multiple controllers, meaning you can group by API area rather than controller
-
Add descriptions to APIs and endpoints using
apiOperation
:// description is optionalpublic get -
Describe endpoint request and response content using
apiRequest
andapiResponse
:// using model classes// each response is associated with a HTTP status codepublic post@fromBody person: Person// using primitive types ("boolean", "double", "int", "number" or "string")public postString@fromBody stuff: string// upload/download files// contentType can be used in any request or response definition, inherits controller or endpoint type by defaultpublic postFile@fromBody fileContents: string// providing custom request/response body examplepublic postCustomInfo@fromBody person: Person// no response content, only a status codepublic deleteThe class
Person
is set as the request and response in several of the examples above. To help the framework provide meaningful request and response examples automatically, you must either:- Provide a public static
example
method in your class, which will be called if found when generating an API spec. (recommended)
-OR-
- Populate your instance in it's constructor with some non null/undefined values.
This is required because object properties are not set until a value is assigned, which makes any sort of reflection impossible.
- Provide a public static
-
Add security schemes to your specification (other than Basic auth) using
apiSecurity
on your authentication filter:This decorator uses the
SecuritySchemeObject
class from theopenapi3-ts
library to describe the security scheme in place. See the source for more information on using this class: SecuritySchemeObject source
YAML Support
For YAML
specification support, you need to install the following packages in your project:
npm install js-yamlnpm install -D @types/js-yaml
Authentication
By default the OpenAPI endpoints do not require authentication. If you wish to apply auth filters when a request is made for a specification, use the useAuthentication
key in the openApi
config:
// build controllers path... appConfig.base = "/api/v1"appConfig.version = "v1"appConfig.openApi.enabled = trueappConfig.openApi.useAuthentication = true // export handler
Testing
For local dev testing and integration with functional tests see the typescript-lambda-api-local package which enables hosting your API using express as a local HTTP server.
Check out this project's dev dependencies to see what is required to test API code. The tests
directory of this repo contains some acceptance tests which will show you how to build mock requests and invoke your application.
Useful links
https://blog.risingstack.com/building-a-node-js-app-with-typescript-tutorial/
https://github.com/jeremydaly/lambda-api
https://codeburst.io/typescript-node-starter-simplified-60c7b7d99e27
https://www.typescriptlang.org/docs/handbook/decorators.html
https://github.com/inversify/InversifyJS
https://www.npmjs.com/package/marky
https://www.meziantou.net/2018/01/11/aspect-oriented-programming-in-typescript