Endpoint Set Up
Acai-TS supports two patterns for defining endpoints: functional pattern (using exported functions with requirements) and decorator pattern (using classes with method decorators). Both approaches follow the "Happy Path Programming" philosophy where validation happens upfront, ensuring your business logic runs cleanly without defensive coding.
Choose Your Pattern
- Functional Pattern: Best for explicit configuration objects and JavaScript/TypeScript mixed codebases
- Decorator Pattern: Best for TypeScript-first development with declarative annotations
Examples
Don't like reading documentation? Then look at our examples, which can run locally!
Functional Pattern
1. Match Function to HTTP Method
Each endpoint file exports functions matching HTTP method names. When an endpoint receives a POST
request, the post
function is invoked.
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 | // File: src/handlers/grower.ts
import { Request, Response } from 'acai-ts';
export const requirements = {}; // discussed in next section below
export const post = async (request: Request, response: Response): Promise<Response> => {
response.body = { message: '[POST] /grower was called' };
return response;
};
export const get = async (request: Request, response: Response): Promise<Response> => {
response.body = { message: '[GET] /grower was called' };
return response;
};
export const patch = async (request: Request, response: Response): Promise<Response> => {
response.body = { message: '[PATCH] /grower was called' };
return response;
};
export const put = async (request: Request, response: Response): Promise<Response> => {
response.body = { message: '[PUT] /grower was called' };
return response;
};
export const delete = async (request: Request, response: Response): Promise<Response> => {
response.body = { message: '[DELETE] /grower was called' };
return response;
};
export const query = async (request: Request, response: Response): Promise<Response> => {
response.body = { message: '[QUERY] /grower, a custom http method, was called' };
return response;
};
|
Each method within the endpoint file can have individual validation requirements. These requirements allow you to test all structural points of the request, with the ability to use JSONSchema and custom middleware to further extend the validation options. Below is an example of a full requirements object:
Info
See the full configuration list, explanation and example of each setting in our Configurations Section.
Tip
If you are already using an openapi.yml
, none of these requirements below are necessary. Ensure your router
has enabled autoValidate
with proper schemaPath
configured and the below requirements are not necessary for any basic structural validation (headers, body, query, params will be checked via openapi.yml). You can still use before
, after
& dataClass
with other custom validations for more advanced use cases.
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 | // File: src/handlers/grower.ts
import { Request, Response } from 'acai-ts';
import { Grower } from './logic/grower';
import * as db from './logic/database';
// Middleware functions
const checkGrowerExists = async (request: Request, response: Response): Promise<void> => {
const result = await db.checkGrowerIdExists(request.pathParameters.id);
if (!result) {
response.code = 404;
response.setError('grower', `grower with id: ${request.pathParameters.id} does not exist.`);
}
};
const filterByRelations = async (request: Request, response: Response): Promise<void> => {
const relations = await db.getRequesterRelations(request.headers['x-requester-id']);
const results: any[] = [];
for (const grower of response.body) {
if (relations.includes(grower.id)) {
results.push(grower);
}
}
response.body = results;
};
export const requirements = {
post: {
requiredHeaders: ['x-onbehalf-of'],
requiredBody: 'post-grower-request'
},
get: {
requiredQuery: ['requester_id']
},
put: {
auth: true,
requiredBody: 'put-grower-request',
timeout: 1500 // will override timeout value set in router config
},
patch: {
auth: true,
requiredBody: 'patch-grower-request',
before: [checkGrowerExists]
},
delete: {
after: [filterByRelations]
}
};
export const post = async (request: Request, response: Response): Promise<Response> => {
response.body = { message: '[POST] /grower was called' };
return response;
};
export const get = async (request: Request, response: Response): Promise<Response> => {
response.body = { message: '[GET] /grower was called' };
return response;
};
export const patch = async (request: Request, response: Response): Promise<Response> => {
response.body = { message: '[PATCH] /grower was called' };
return response;
};
export const put = async (request: Request, response: Response): Promise<Response> => {
response.body = { message: '[PUT] /grower was called' };
return response;
};
export const delete = async (request: Request, response: Response): Promise<Response> => {
response.body = { message: '[DELETE] /grower was called' };
return response;
};
export const query = async (request: Request, response: Response): Promise<Response> => {
response.body = { message: '[QUERY] /grower, a custom http method, was called' };
return response;
};
|
Decorator Pattern
1. Create Class Extending BaseEndpoint
The decorator pattern uses classes that extend BaseEndpoint
with method decorators for configuration.
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 | // File: src/handlers/grower.ts
import 'reflect-metadata';
import { BaseEndpoint, Request, Response } from 'acai-ts';
export class GrowerEndpoint extends BaseEndpoint {
async post(request: Request, response: Response): Promise<Response> {
response.body = { message: '[POST] /grower was called' };
return response;
}
async get(request: Request, response: Response): Promise<Response> {
response.body = { message: '[GET] /grower was called' };
return response;
}
async patch(request: Request, response: Response): Promise<Response> {
response.body = { message: '[PATCH] /grower was called' };
return response;
}
async put(request: Request, response: Response): Promise<Response> {
response.body = { message: '[PUT] /grower was called' };
return response;
}
async delete(request: Request, response: Response): Promise<Response> {
response.body = { message: '[DELETE] /grower was called' };
return response;
}
async query(request: Request, response: Response): Promise<Response> {
response.body = { message: '[QUERY] /grower, a custom http method, was called' };
return response;
}
}
|
2. Add Method Decorators (optional)
Use decorators to configure validation, authentication, middleware, and timeouts for each HTTP method.
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75 | // File: src/handlers/grower.ts
import 'reflect-metadata';
import { BaseEndpoint, Validate, Auth, Before, After, Timeout, Request, Response } from 'acai-ts';
import { Grower } from './logic/grower';
import * as db from './logic/database';
// Middleware functions
const checkGrowerExists = async (request: Request, response: Response): Promise<void> => {
const result = await db.checkGrowerIdExists(request.pathParameters.id);
if (!result) {
response.code = 404;
response.setError('grower', `grower with id: ${request.pathParameters.id} does not exist.`);
}
};
const filterByRelations = async (request: Request, response: Response): Promise<void> => {
const relations = await db.getRequesterRelations(request.headers['x-requester-id']);
const results: any[] = [];
for (const grower of response.body) {
if (relations.includes(grower.id)) {
results.push(grower);
}
}
response.body = results;
};
const logRequest = async (request: Request, response: Response): Promise<void> => {
console.log(`${request.method} ${request.path} - ${new Date().toISOString()}`);
};
export class GrowerEndpoint extends BaseEndpoint {
@Validate({ requiredHeaders: ['x-onbehalf-of'], requiredBody: 'post-grower-request' })
@Before(logRequest)
async post(request: Request, response: Response): Promise<Response> {
response.body = { message: '[POST] /grower was called' };
return response;
}
@Validate({ requiredQuery: ['requester_id'] })
@Before(logRequest)
async get(request: Request, response: Response): Promise<Response> {
response.body = { message: '[GET] /grower was called' };
return response;
}
@Auth()
@Validate({ requiredBody: 'patch-grower-request' })
@Before(checkGrowerExists)
@Before(logRequest)
async patch(request: Request, response: Response): Promise<Response> {
response.body = { message: '[PATCH] /grower was called' };
return response;
}
@Auth()
@Validate({ requiredBody: 'put-grower-request' })
@Before(logRequest)
@Timeout(1500)
async put(request: Request, response: Response): Promise<Response> {
response.body = { message: '[PUT] /grower was called' };
return response;
}
@Before(logRequest)
@After(filterByRelations)
async delete(request: Request, response: Response): Promise<Response> {
response.body = { message: '[DELETE] /grower was called' };
return response;
}
async query(request: Request, response: Response): Promise<Response> {
response.body = { message: '[QUERY] /grower, a custom http method, was called' };
return response;
}
}
|
Available Decorators
Decorator |
Purpose |
Example |
@Validate() |
Request validation |
@Validate({ requiredBody: 'UserSchema' }) |
@Auth() |
Authentication required |
@Auth() or @Auth(false) |
@Before() |
Pre-processing middleware |
@Before(authMiddleware, loggingMiddleware) |
@After() |
Post-processing middleware |
@After(loggingMiddleware) |
@Timeout() |
Method timeout |
@Timeout(5000) |
Decorator Execution Order
@Before
middleware (runs first)
@Auth
authentication (router's withAuth
middleware)
@Validate
validation
- HTTP method with
@Timeout
@After
middleware (runs last)
Pattern Comparison
Feature |
Functional Pattern |
Decorator Pattern |
Configuration |
requirements object |
Method decorators |
TypeScript Required |
No |
Yes |
File Structure |
Export functions |
Export class |
Middleware |
Arrays in requirements |
Multiple decorators |
Reusability |
High (shared requirements) |
Moderate |
Co-location |
Separate from methods |
Inline with methods |