Request routing for AWS Lambda running Nodejs, written in Typescript
Motivation: Existing routing libraries are inefficient. This library uses a trie data structure with local caching to improve lookup and response time. (More latency data to come)
npm install @pyriter/unrest- Define routes
- Define controllers
- Type the request body
- Support for URL parameters
- Query string parameter handling
- Request body validation
- Response building with status codes
- AWS Lambda integration
Set the "noStrictGenericChecks" to true in your tsconfig to avoid typescript errors
{
"compilerOptions": {
...
"noStrictGenericChecks": true,
...
}
}import { StatusType, Unrest, MethodType } from "@pyriter/unrest";
import { APIGatewayProxyEvent } from "aws-lambda";
import { UnrestResponse } from "./unrestResponse";
import { RequestProps } from "./route";
class ApiServiceHandler {
private readonly unrest: Unrest;
constructor() {
this.unrest = Unrest.builder()
.withRoute({
method: MethodType.GET,
path: "/api/v1/ping",
handler: async (): Promise<Response> => {
return Response.builder()
.withStatusCode(StatusType.OK)
.withBody({
message: "success"
}).build();
}
})
.build();
}
async handle(event: APIGatewayProxyEvent): Promise<UnrestResponse> {
return await this.unrest.execute(event);
}
}Create controllers to organize your API endpoints:
import { RequestProps, Response, StatusType, Unrest, MethodType } from '@pyriter/unrest';
export class UserController {
constructor(private readonly unrest: Unrest) {
this.unrest.withRoutes([
{
method: MethodType.GET,
path: '/api/v1/users',
handler: this.getAllUsers,
thisReference: this,
},
{
method: MethodType.GET,
path: '/api/v1/users/{userId}',
handler: this.getUserById,
thisReference: this,
},
{
method: MethodType.POST,
path: '/api/v1/users',
handler: this.createUser,
thisReference: this,
},
{
method: MethodType.PUT,
path: '/api/v1/users/{userId}',
handler: this.updateUser,
thisReference: this,
},
{
method: MethodType.DELETE,
path: '/api/v1/users/{userId}',
handler: this.deleteUser,
thisReference: this,
},
]);
}
async getAllUsers(request: RequestProps<undefined>): Promise<Response<User[] | string>> {
try {
const { apiGatewayEvent, queryStringParams } = request;
const { limit, offset } = queryStringParams;
// Your business logic here
const users = await this.userService.getUsers({ limit, offset });
return Response.builder<User[]>()
.withStatusCode(StatusType.OK)
.withBody(users)
.build();
} catch (error) {
return Response.builder<string>()
.withStatusCode(StatusType.INTERNAL_SERVER_ERROR)
.withBody(`Error fetching users: ${error}`)
.build();
}
}
async getUserById(request: RequestProps<undefined>): Promise<Response<User | string>> {
try {
const { urlParams } = request;
const userId = urlParams.userId || '';
if (!userId) {
return Response.builder<string>()
.withStatusCode(StatusType.BAD_REQUEST)
.withBody('userId is required')
.build();
}
const user = await this.userService.getUserById(userId);
if (!user) {
return Response.builder<string>()
.withStatusCode(StatusType.NOT_FOUND)
.withBody('User not found')
.build();
}
return Response.builder<User>()
.withStatusCode(StatusType.OK)
.withBody(user)
.build();
} catch (error) {
return Response.builder<string>()
.withStatusCode(StatusType.INTERNAL_SERVER_ERROR)
.withBody(`Error fetching user: ${error}`)
.build();
}
}
async createUser(request: RequestProps<CreateUserRequest>): Promise<Response<User | string>> {
try {
const { body, apiGatewayEvent } = request;
const { name, email, role } = body;
// Validate required fields
if (!name || !email) {
return Response.builder<string>()
.withStatusCode(StatusType.BAD_REQUEST)
.withBody('Name and email are required')
.build();
}
const user = await this.userService.createUser({ name, email, role });
return Response.builder<User>()
.withStatusCode(StatusType.CREATED)
.withBody(user)
.build();
} catch (error) {
return Response.builder<string>()
.withStatusCode(StatusType.INTERNAL_SERVER_ERROR)
.withBody(`Error creating user: ${error}`)
.build();
}
}
async updateUser(request: RequestProps<UpdateUserRequest>): Promise<Response<User | string>> {
try {
const { urlParams, body, apiGatewayEvent } = request;
const userId = urlParams.userId || '';
const { name, email, role } = body;
if (!userId) {
return Response.builder<string>()
.withStatusCode(StatusType.BAD_REQUEST)
.withBody('userId is required')
.build();
}
const updatedUser = await this.userService.updateUser(userId, { name, email, role });
return Response.builder<User>()
.withStatusCode(StatusType.OK)
.withBody(updatedUser)
.build();
} catch (error) {
return Response.builder<string>()
.withStatusCode(StatusType.INTERNAL_SERVER_ERROR)
.withBody(`Error updating user: ${error}`)
.build();
}
}
async deleteUser(request: RequestProps<undefined>): Promise<Response<boolean | string>> {
try {
const { urlParams } = request;
const userId = urlParams.userId || '';
if (!userId) {
return Response.builder<string>()
.withStatusCode(StatusType.BAD_REQUEST)
.withBody('userId is required')
.build();
}
await this.userService.deleteUser(userId);
return Response.builder<boolean>()
.withStatusCode(StatusType.OK)
.withBody(true)
.build();
} catch (error) {
return Response.builder<string>()
.withStatusCode(StatusType.INTERNAL_SERVER_ERROR)
.withBody(`Error deleting user: ${error}`)
.build();
}
}
}Wire up your controllers in a main service handler:
import { APIGatewayProxyEvent } from 'aws-lambda';
import { Unrest, UnrestResponse } from '@pyriter/unrest';
export class ServiceHandler {
constructor(
private readonly userController: UserController,
private readonly orderController: OrderController,
private readonly productController: ProductController,
private readonly unrest: Unrest,
) {
// Controllers have configured their routes, build the Unrest instance
}
async handle(event: APIGatewayProxyEvent): Promise<UnrestResponse> {
return await this.unrest.execute(event);
}
}Access URL parameters using request.urlParams:
async getUserById(request: RequestProps<undefined>): Promise<Response<User | string>> {
const { urlParams } = request;
const userId = urlParams.userId || '';
if (!userId) {
return Response.builder<string>()
.withStatusCode(StatusType.BAD_REQUEST)
.withBody('userId is required')
.build();
}
// Use userId in your business logic
}Access query parameters using request.queryStringParams:
async getUsers(request: RequestProps<undefined>): Promise<Response<User[] | string>> {
const { queryStringParams } = request;
const { limit = '10', offset = '0', sortBy = 'name' } = queryStringParams;
const users = await this.userService.getUsers({
limit: parseInt(limit),
offset: parseInt(offset),
sortBy
});
return Response.builder<User[]>()
.withStatusCode(StatusType.OK)
.withBody(users)
.build();
}Type your request body and access it via request.body:
interface CreateUserRequest {
name: string;
email: string;
role?: string;
}
async createUser(request: RequestProps<CreateUserRequest>): Promise<Response<User | string>> {
const { body } = request;
const { name, email, role } = body;
// Validate and process the request body
if (!name || !email) {
return Response.builder<string>()
.withStatusCode(StatusType.BAD_REQUEST)
.withBody('Name and email are required')
.build();
}
const user = await this.userService.createUser({ name, email, role });
return Response.builder<User>()
.withStatusCode(StatusType.CREATED)
.withBody(user)
.build();
}Use the Response.builder() to construct standardized responses:
// Success response
return Response.builder<User>()
.withStatusCode(StatusType.OK)
.withBody(user)
.build();
// Error response
return Response.builder<string>()
.withStatusCode(StatusType.BAD_REQUEST)
.withBody('Validation error: field is required')
.build();
// Created response
return Response.builder<User>()
.withStatusCode(StatusType.CREATED)
.withBody(newUser)
.build();
// Not found response
return Response.builder<string>()
.withStatusCode(StatusType.NOT_FOUND)
.withBody('Resource not found')
.build();Implement consistent error handling across your controllers:
async handleRequest<T>(requestFn: () => Promise<T>): Promise<Response<T | string>> {
try {
const result = await requestFn();
return Response.builder<T>()
.withStatusCode(StatusType.OK)
.withBody(result)
.build();
} catch (error) {
console.error('Request failed:', error);
if (error.name === 'ValidationError') {
return Response.builder<string>()
.withStatusCode(StatusType.BAD_REQUEST)
.withBody(`Validation error: ${error.message}`)
.build();
}
if (error.name === 'NotFoundError') {
return Response.builder<string>()
.withStatusCode(StatusType.NOT_FOUND)
.withBody(error.message)
.build();
}
return Response.builder<string>()
.withStatusCode(StatusType.INTERNAL_SERVER_ERROR)
.withBody('Internal server error')
.build();
}
}The routing library itself. It can execute an APIGatewayEvent and invoke the desired controller.
Returns the builder for creating an instance of the unrest object.
Enum for HTTP methods:
MethodType.GETMethodType.POSTMethodType.PUTMethodType.DELETEMethodType.PATCH
Enum for HTTP status codes:
StatusType.OK(200)StatusType.CREATED(201)StatusType.BAD_REQUEST(400)StatusType.UNAUTHORIZED(401)StatusType.FORBIDDEN(403)StatusType.NOT_FOUND(404)StatusType.INTERNAL_SERVER_ERROR(500)
Generic interface for request properties:
urlParams: URL path parametersqueryStringParams: Query string parametersbody: Request body (typed as T)headers: Request headersapiGatewayEvent: Original AWS API Gateway eventmethod: HTTP methodpath: Request path
Generic response interface:
statusCode: HTTP status codebody: Response body (typed as T)
Builder pattern for constructing responses:
.withStatusCode(code): Set HTTP status code.withBody(data): Set response body.build(): Build the final response
Unrest uses a trie data structure for efficient route matching and includes local caching to improve lookup and response times. The library is designed to minimize latency in AWS Lambda environments.
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License.