README
λ thirty
Lightweight extensions that make AWS Lambda functions easy to develop, testable and type safe.
In the system of Greek numerals lambda has a value of 30 > https://en.wikipedia.org/wiki/Lambda
Install
npm install thirty
Usage
// handler.ts
import { APIGatewayProxyEvent } from 'aws-lambda';
import { compose, eventType } from 'thirty/core';
import { parseJson } from 'thirty/parseJson';
import { verifyJwt, tokenFromHeaderFactory } from 'thirty/verifyJwt';
import { handleHttpErrors } from 'thirty/handleHttpErrors';
import { inject } from 'thirty/inject';
export const handler = compose(
eventType<APIGatewayProxyEvent>(),
inject({
authService: authServiceFactory,
userService: userServiceFactory,
}),
handleHttpErrors(),
parseJson(),
verifyJwt({
getToken: tokenFromHeaderFactory(),
getSecretOrPublic: ({ deps }) => deps.authService.getSecret(),
}),
)(async event => {
const { userService } = event.deps;
const user = await userService.createUser(event.jsonObject);
return {
statusCode: 201,
body: JSON.stringify(user),
};
});
Testing
The compose
d handler function also provides a reference to the actual handler
via the actual
property:
// handler.spec.ts
import { handler } from './handler';
it('should return created user', async () => {
const user = {
/*...*/
};
const eventMock = {
deps: { userService: userServiceMock /* ..*/ },
/* ..*/
};
const { statusCode, body } = await handler.actual(eventMock);
expect(statusCode).toBe(201);
expect(body).toEqual(user);
});
compose
compose
is a common implementation of Function_composition
and the heart of thirty.
On top of that it provides typings so that TypeScript can infer the typings provided by the passed middlewares.
export const handler = compose(
eventType<{ someType: string }>(),
someAuthMiddleware(),
)(async event => {
event.someType;
event.user;
});
It also exposes a reference to the argument of the composed function:
const actual = async () => {};
export const handler = compose()(actual);
// ...
handler.actual === actual; // true
Middlewares
handleCors
handleCors
is a middleware that creates a preflight response to OPTIONS
requests and adds CORS headers to any other
request.
Requires
sanitizeHeaders
middleware
import { sanitizeHeaders } from 'thirty/sanitizeHeaders';
import { handleCors } from 'thirty/handleCors';
export const handler = compose(
eventType<APIGatewayProxyEvent>(),
sanitizeHeaders(),
handleCors(),
)(async event => {
// ...
});
CorsOptions
The above example would create an error response that would look like:
{
"statusCode": 400,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"error\":\"Parameter x missing\"}"
}
inject
inject
is a middleware that provides lightweight dependency injection with the possibility of circular dependencies.
In order to create a dependency injection container, just define an object, where its properties refer to factory methods.
import { inject } from 'thirty/inject';
export const handler = compose(
eventType<APIGatewayProxyEvent>(),
inject({
authService: authServiceFactory,
userService: userServiceFactory,
}),
)(async event => {
const { userService } = event.deps;
// ...
});
Each factory gets a reference to the created dependency container:
export type AuthServiceDeps = { userService: UserService };
export type AuthService = ReturnType<typeof authServiceFactory>;
export const authServiceFactory = ({ userService }: AuthServiceDeps) => ({
authenticate() {
const user = userService.getUser();
// ...
},
});
This makes it easy to mock and test the actual handler:
// handler.spec.ts
it('should return created user', async () => {
const eventMock = {
deps: { authService: authServiceMock, userService: userServiceMock },
/* ..*/
};
const result = await handler.actual(eventMock);
// assertion goes here
});
parseCookie
parseCookie
is a middleware that parses the event cookie header and extends the event object by a cookie object:
import { parseCookie } from 'thirty/parseCookie';
export const handler = compose(
eventType<{ someType: string }>(),
parseCookie(),
)(async event => {
event.cookie;
});
parseJson
parseJson
is a middleware that parses the event body and extends the event object by a jsonBody
object:
import { parseCookie } from 'thirty/parseCookie';
export const handler = compose(
eventType<{ someType: string }>(),
parseJson(),
)(async event => {
event.jsonBody;
});
registerHttpErrorHandler
registerHttpErrorHandler
is a middleware that wraps the actual handler, catches all errors and creates an error response:
import { registerHttpErrorHandler } from 'thirty/registerHttpErrorHandler';
import { BadRequestError } from 'thirty/errors';
export const handler = compose(
eventType<{ someType: string }>(),
registerHttpErrorHandler({
logger: console,
backlist: [{ statusCode: 401, message: 'Alternative message' }],
}),
)(async event => {
throw new BadRequestError('Parameter x missing');
});
HttpErrorHandlerOptions
sanitizeHeaders
sanitizeHeaders
is a middleware that lower cases all header properties and stores them in a new event.sanitizedHeaders
object.
This is necessary because the header properties in event.headers
aren't consolidated. Which means they are deserialized
as set in the header request.
import { sanitizeHeaders } from 'thirty/sanitizeHeaders';
export const handler = compose(
eventType<{ someType: string }>(),
sanitizeHeaders(),
)(async event => {
event.sanitizedHeaders;
});
decodeParameters
decodeParameters
is a middleware that decodes all parameter values with decodeURIComponent
and stores them in
event.decodedPathParameters
,event.decodedQueryParameters
, event.decodedMultiValueQueryParameters
.
import { decodeParameters } from 'thirty/decodeParameters';
export const handler = compose(
eventType<{ someType: string }>(),
decodeParameters(),
)(async event => {
event.decodeParameters;
event.decodedQueryParameters;
event.decodedMultiValueQueryParameters;
});
verifyJwt
verifyJwt
is a authentication middleware, which extends the event object by a user
object and throws an
UnauthorizedError
if the client is not authorized. Under the hood it uses the jsonwebtoken
library.
import { verifyJwt } from 'thirty/verifyJwt';
export const handler = compose(
eventType<{ someType: string }>(),
verifyJwt({
getToken: event => event.headers.Authorization.split(' ')[1],
getSecretOrPublic: ({ deps, event, decodedJwt }) => someSecretOrPublic,
}),
)(async event => {
event.user;
});
thirty/verifyJwt
already provides factory functions to retrieve the token from headers or cookie:
tokenFromHeaderFactory
expects a header name (default is'Authorization'
).Requires
sanitizeHeaders
middlewareimport { tokenFromHeaderFactory } from 'thirty/verifyJwt'; { getToken: tokenFromHeaderFactory(); }
tokenFromCookieFactory
requiresparseCookie
middleware and expects a key for cookie entry (default is'authentication'
).import { tokenFromCookieFactory } from 'thirty/verifyJwt'; { getToken: tokenFromCookieFactory(); }
Options API
getToken
- Function that expects the token that should be validated.getSecretOrPublic
- Secret or public key provider for verifying token.- All options that can be passed to jsonwebtoken's
verify
verifyXsrfToken
verifyXsrfToken
is a middleware that checks the XSRF Token provided in the request headers. It uses the csrf
library.
Requires
sanitizeHeaders
middlware
import { verifyXsrfToken } from 'thirty/verifyXsrfToken';
export const handler = compose(
eventType<{ someType: string }>(),
verifyXsrfToken({
getSecret: ({ event }) => secret,
}),
)(async event => {
// ...
});
Routing
routing
is a wrapper for the actual handler function to define multiple routes and their corresponding handlers:
import { createRoutes } from 'thirty/createRoutes';
export const handler = compose(
eventType<APIGatewayProxyEvent>(),
inject({
userService: () => ({
/*...*/
}),
}),
parseJson(),
)(
createRoutes(router => {
router.get('/users', ({ deps }) => {
return {
statusCode: 200,
body: JSON.stringify(deps.userService.getUsers()),
};
});
router.post('/users', async ({ deps, jsonBody }) => {
return {
statusCode: 201,
body: JSON.stringify(deps.userService.createUser(jsonBody)),
};
});
router.get('/users/:id', async ({ deps, params }) => {
const user = deps.userService.getUserById(params.id);
if (user) {
return {
statusCode: 200,
body: JSON.stringify(user),
};
}
return {
statusCode: 404,
};
});
}),
);