🎨 Decorator Pattern
Acai-TS provides a powerful decorator-based approach for defining routes, middleware, and validation. Decorators offer a clean, declarative way to configure your endpoints without boilerplate code.
TypeScript Configuration Required
To use decorators, ensure your tsconfig.json
includes:
| {
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2020"
}
}
|
🚀 Quick Start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 | import 'reflect-metadata';
import { BaseEndpoint, Validate, Auth, Before, After, Timeout, Response, Request } from 'acai-ts';
// File: src/handlers/users/{id}.ts
// Maps to GET/PUT/DELETE /users/{id}
export class UserEndpoint extends BaseEndpoint {
@Auth()
@Timeout(5000)
async get(request: Request, response: Response): Promise<Response> {
response.body = { id: request.pathParameters.id, name: 'John Doe' };
return response;
}
}
// File: src/handlers/users.ts
// Maps to GET/POST /users
export class UsersEndpoint extends BaseEndpoint {
@Validate({
requiredBody: 'CreateUserRequest'
})
@Before(async (request: Request, response: Response) => {
console.log('Creating user:', request.body.email);
})
@After(async (request: Request, response: Response) => {
console.log('User created successfully');
})
async post(request: Request, response: Response): Promise<Response> {
const { name, email } = request.body;
response.code = 201;
response.body = { id: Math.random(), name, email };
return response;
}
}
|
🏷️ Available Decorators
📁 File-Based Routing (No @Route Decorator)
Acai-TS uses file-based routing instead of @Route
decorators. Routes are determined by your file structure:
File Structure → Routes:
| src/handlers/
├── users.ts → /users (GET, POST, PUT, DELETE)
├── users/{id}.ts → /users/{id} (GET, PUT, DELETE)
└── products/index.ts → /products
|
HTTP Methods:
Define methods in your BaseEndpoint
class:
| export class UsersEndpoint extends BaseEndpoint {
async get(request: Request, response: Response): Promise<Response> { /* GET /users */ }
async post(request: Request, response: Response): Promise<Response> { /* POST /users */ }
async put(request: Request, response: Response): Promise<Response> { /* PUT /users */ }
async delete(request: Request, response: Response): Promise<Response> { /* DELETE /users */ }
}
|
✓ @Validate
Adds request validation using OpenAPI schemas or JSON Schema.
| @Validate(validationConfig: ValidationConfig)
|
Examples:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | // OpenAPI schema reference
@Validate({ requiredBody: 'CreateUserRequest' })
// Required headers
@Validate({ requiredHeaders: ['x-api-key', 'authorization'] })
// Required query parameters
@Validate({ requiredQuery: ['page', 'limit'] })
// Direct JSON Schema
@Validate({
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 2, maxLength: 50 },
email: { type: 'string', format: 'email' }
}
}
})
// Multiple validations
@Validate({
requiredBody: 'CreateUserRequest',
requiredHeaders: ['authorization']
})
|
🔐 @Auth
Marks a method as requiring authentication using the router's global withAuth
middleware.
| @Auth(required?: boolean)
|
Setup Router with Auth Middleware:
| const router = new Router({
basePath: '/api/v1',
routesPath: './src/handlers/**/*.ts',
withAuth: async (request: Request, response: Response) => {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token || !validateJWT(token)) {
response.code = 401;
response.setError('auth', 'Invalid or missing authentication token');
}
}
});
|
Examples:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | export class UsersEndpoint extends BaseEndpoint {
@Auth() // Requires authentication (default: required=true)
async get(request: Request, response: Response): Promise<Response> {
response.body = { users: [] };
return response;
}
@Auth(false) // Explicitly disable auth requirement
async post(request: Request, response: Response): Promise<Response> {
response.body = { message: 'Public endpoint' };
return response;
}
// No @Auth decorator = no auth requirement
async options(request: Request, response: Response): Promise<Response> {
response.body = { message: 'CORS preflight' };
return response;
}
}
|
⬅️ @Before
Adds middleware that runs before the main handler.
| @Before(middleware1, middleware2, ...)
|
Examples:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | // Single middleware
const logRequest = async (request: Request, response: Response) => {
console.log(`${request.method} ${request.path} - ${new Date().toISOString()}`);
};
@Before(logRequest)
async get(request: Request, response: Response): Promise<Response> {
// Handler code
}
// Multiple middlewares (execute in order)
const rateLimiter = async (request: Request, response: Response) => {
const clientIp = request.headers['x-forwarded-for'] || 'unknown';
if (await isRateLimited(clientIp)) {
response.code = 429;
response.setError('rate_limit', 'Too many requests');
}
};
const authCheck = async (request: Request, response: Response) => {
if (!request.headers.authorization) {
response.code = 401;
response.setError('auth', 'Unauthorized');
}
};
@Before(rateLimiter, authCheck) // Executes: rateLimiter → authCheck → handler
async post(request: Request, response: Response): Promise<Response> {
// Handler code
}
|
➡️ @After
Adds middleware that runs after the main handler.
| @After(middleware1, middleware2, ...)
|
Examples:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | // Single middleware
const addTimestamp = async (request: Request, response: Response) => {
response.body.timestamp = new Date().toISOString();
};
@After(addTimestamp)
async get(request: Request, response: Response): Promise<Response> {
response.body = { data: 'value' };
return response;
}
// Multiple middlewares (execute in order)
const addSecurityHeaders = async (request: Request, response: Response) => {
response.setHeader('X-Content-Type-Options', 'nosniff');
response.setHeader('X-Frame-Options', 'DENY');
};
const sanitizeResponse = async (request: Request, response: Response) => {
if (response.body?.users) {
response.body.users = response.body.users.map(user => ({
...user,
password: undefined // Remove sensitive data
}));
}
};
@After(addSecurityHeaders, sanitizeResponse) // Executes: handler → addSecurityHeaders → sanitizeResponse
async get(request: Request, response: Response): Promise<Response> {
// Handler code
}
|
⏱️ @Timeout
Sets a timeout for the endpoint.
| @Timeout(milliseconds: number)
|
Examples:
| @Timeout(5000) // 5 second timeout
@Timeout(30000) // 30 second timeout for heavy operations
@Timeout(1000) // 1 second timeout for quick operations
|
🤝 Combining Decorators
Decorators can be combined and will execute in the following order:
- @Before - Custom middleware (runs first)
- @Auth - Authentication check (router's
withAuth
middleware)
- @Validate - Request validation
- Handler - Your main function with
@Timeout
- @After - Post-processing middleware
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | // File: src/handlers/orders.ts
const enrichOrder = async (request: Request, response: Response) => {
request.body.orderId = generateOrderId();
request.body.timestamp = Date.now();
};
const sendConfirmation = async (request: Request, response: Response) => {
await sendOrderConfirmation(response.body.orderId);
};
export class OrdersEndpoint extends BaseEndpoint {
@Before(enrichOrder) // Runs first
@Auth() // Auth middleware runs after Before
@Validate({ // Validates request
requiredBody: 'CreateOrderRequest'
})
@Timeout(10000) // Sets timeout
@After(sendConfirmation) // Runs last
async post(request: Request, response: Response): Promise<Response> {
const order = await processOrder(request.body);
response.code = 201;
response.body = order;
return response;
}
}
|
🌐 Router Configuration for Decorators
Configure your router for file-based routing with decorators:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | import 'reflect-metadata';
import { Router } from 'acai-ts';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
const router = new Router({
basePath: '/api/v1',
routesPath: './src/handlers/**/*.ts', // File-based routing
schemaPath: './openapi.yml', // Optional: OpenAPI validation
timeout: 30000,
outputError: true,
withAuth: async (request, response) => { // Global auth middleware
// Your JWT validation logic here
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token || !validateJWT(token)) {
response.code = 401;
response.setError('auth', 'Invalid or missing authentication token');
}
}
});
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
return await router.route(event);
};
|
🌟 Best Practices
1️⃣ Keep Auth Simple
| // ✅ Good - Use @Auth as boolean flag
@Auth() // Uses router's withAuth middleware
@Auth(false) // Explicitly disable auth
// ❌ Avoid - @Auth doesn't take functions (that's the old API)
// @Auth(async (request) => { /* complex logic */ })
// Instead, put complex logic in router's withAuth middleware
|
2️⃣ Use Multiple @Before/@After for Different Concerns
| // ✅ Good - Separate concerns
@Before(logRequest)
@Before(validateBusinessRules)
@Before(enrichRequestData)
// ❌ Avoid - Single decorator doing everything
@Before(async (request) => {
// logging + validation + enrichment all in one
})
|
3️⃣ Consistent Error Handling
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | // Handle errors in middleware functions
const authMiddleware = async (request: Request, response: Response) => {
try {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token || !await validateToken(token)) {
response.code = 401;
response.setError('auth', 'Invalid authentication');
}
} catch (error) {
console.error('Auth error:', error);
response.code = 401;
response.setError('auth', 'Authentication failed');
}
};
@Before(authMiddleware) // Use in @Before instead of @Auth for complex logic
|
4️⃣ Type Safety
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | interface CreateUserRequest {
name: string;
email: string;
age?: number;
}
// File: src/handlers/users.ts
export class UsersEndpoint extends BaseEndpoint {
@Validate({
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0 }
}
}
})
async post(request: Request, response: Response): Promise<Response> {
// TypeScript knows request.body is CreateUserRequest when properly typed
const { name, email, age } = request.body as CreateUserRequest;
response.body = { id: 123, name, email, age };
return response;
}
}
|
🔄 Migration from Functional Approach
🔴 Before (Functional Pattern)
1
2
3
4
5
6
7
8
9
10
11
12 | export const requirements = {
post: {
before: [authMiddleware],
requiredBody: 'CreateUserSchema',
timeout: 5000
}
};
export const post = async (request: Request, response: Response) => {
response.body = { id: 123, ...request.body };
return response;
};
|
🟢 After (Class-Based with Decorators)
| // File: src/handlers/users.ts
export class UsersEndpoint extends BaseEndpoint {
@Auth() // Uses router's withAuth middleware
@Validate({ requiredBody: 'CreateUserSchema' })
@Timeout(5000)
async post(request: Request, response: Response): Promise<Response> {
response.body = { id: 123, ...request.body };
return response;
}
}
|
📦 Common Patterns
📝 CRUD Operations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 | // File: src/handlers/users.ts - Handles /users
export class UsersEndpoint extends BaseEndpoint {
@Auth()
async get(request: Request, response: Response): Promise<Response> {
const users = await getUserList();
response.body = { users };
return response;
}
@Auth()
@Validate({ requiredBody: 'CreateUserSchema' })
async post(request: Request, response: Response): Promise<Response> {
const user = await createUser(request.body);
response.code = 201;
response.body = user;
return response;
}
}
// File: src/handlers/users/{id}.ts - Handles /users/{id}
export class UserEndpoint extends BaseEndpoint {
@Auth()
async get(request: Request, response: Response): Promise<Response> {
const user = await getUserById(request.pathParameters.id);
response.body = user;
return response;
}
@Auth()
@Validate({ requiredBody: 'UpdateUserSchema' })
async put(request: Request, response: Response): Promise<Response> {
const user = await updateUser(request.pathParameters.id, request.body);
response.body = user;
return response;
}
@Auth()
async delete(request: Request, response: Response): Promise<Response> {
await deleteUser(request.pathParameters.id);
response.code = 204;
return response;
}
}
|
For more detailed examples and advanced usage patterns, see our troubleshooting guide and the example code on GitHub.