@pyriter/unrest
v2.8.0
Published
Request routing library for NodeJs. Written for AWS lambda.
Maintainers
Readme
Unrest
Description
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)
Install
npm install @pyriter/unrestFeatures
- 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
One Time Setup
Set the "noStrictGenericChecks" to true in your tsconfig to avoid typescript errors
{
"compilerOptions": {
...
"noStrictGenericChecks": true,
...
}
}Usage
Basic Setup
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);
}
}Controller Pattern
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();
}
}
}Service Handler Integration
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);
}
}Request Handling
URL Parameters
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
}Query String Parameters
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();
}Request Body
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();
}Response Building
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();Error Handling
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();
}
}API Reference
Unrest
The routing library itself. It can execute an APIGatewayEvent and invoke the desired controller.
Unrest.builder()
Returns the builder for creating an instance of the unrest object.
MethodType
Enum for HTTP methods:
MethodType.GETMethodType.POSTMethodType.PUTMethodType.DELETEMethodType.PATCH
StatusType
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)
RequestProps
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
Response
Generic response interface:
statusCode: HTTP status codebody: Response body (typed as T)
Response.builder()
Builder pattern for constructing responses:
.withStatusCode(code): Set HTTP status code.withBody(data): Set response body.build(): Build the final response
Performance
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.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License.
