Core Concepts
Standardized Response Patterns
A consistent response format for all your API endpoints. Every response includes a success boolean, making it easy for frontend developers to handle responses without checking HTTP status codes.
Overview
Every API response includes a success boolean. Simple, consistent, and type-safe.
The Standard Format
1{
2 success: true,
3 data: {...}
4}1{
2 success: false,
3 error: '...',
4 code: '...'
5}1{
2 success: true,
3 data: [...],
4 pagination: {
5 page: 1,
6 limit: 10,
7 total: 95,
8 totalPages: 10,
9 hasNext: true,
10 hasPrev: false
11 }
12}1{
2 success: false,
3 error: 'Validation Failed',
4 code: 'VALIDATION_ERROR',
5 errors: [
6 {
7 field: 'email',
8 message: 'Invalid email format',
9 code: 'INVALID_EMAIL'
10 }
11 ]
12}The success boolean is always present, and that's what makes it work.
Frontend developers waste hours dealing with inconsistent API responses. We fixed that. Without a standard, you're checking HTTP status codes everywhere: response.status === 404, response.status === 400, and still guessing the response format.
The Problem
- Each endpoint returns a different format
- Errors come in different shapes
- No TypeScript type safety
- Custom handling for every endpoint
The Solution
- One format for all endpoints
- Check result.success instead of status codes
- TypeScript knows what properties exist
- One handler works everywhere
1app.get('/users/:id', async (req, res) => {
2 const user = await getUser(req.params.id);
3
4 if (!user) {
5 return res.notFound('User');
6 }
7
8 res.success(user);
9});Consistent
Same format everywhere
Type-Safe
TypeScript knows the shape
Simple
One handler, zero overhead
Note: This is a recommended pattern, not a requirement. Use it to create consistent, maintainable APIs.
How It Works
Backend: Send Responses
1app.get('/users/:id', async (req, res) => {
2 const user = await getUser(req.params.id);
3
4 if (!user) {
5 return res.notFound('User');
6 // Returns: { success: false, error: 'Not Found' }
7 }
8
9 res.success(user);
10 // Returns: { success: true, data: {...} }
11});Frontend: Use Responses
1const result = await fetch('/users/123').then(r => r.json());
2
3if (result.success) {
4 // TypeScript knows: result.data exists
5 console.log(result.data.name); // ✅ Works!
6} else {
7 // TypeScript knows: result.error exists
8 console.log(result.error); // ✅ Works!
9 // No need to check response.status!
10}The Benefit
Every response has a success boolean. TypeScript automatically knows:
- If
success: true, thendataexists - If
success: false, thenerrorexists - No need to check HTTP status codes, just check
result.success
Common Patterns
1// Get list of items
2app.get('/users', async (req, res) => {
3 const users = await getUsers();
4 res.success(users);
5 // Returns: { success: true, data: [...] }
6});
7
8// Get single item
9app.get('/users/:id', async (req, res) => {
10 const user = await getUser(req.params.id);
11 res.success(user);
12 // Returns: { success: true, data: {...} }
13});
14
15// Create new item
16app.post('/users', async (req, res) => {
17 const user = await createUser(req.body);
18 res.created(user, `/users/${user.id}`);
19 // Returns: { success: true, data: {...} }
20 // Also sets HTTP 201 status and Location header
21});
22
23// Delete item (no content)
24app.delete('/users/:id', async (req, res) => {
25 await deleteUser(req.params.id);
26 res.noContent();
27 // Returns: HTTP 204 (no body)
28});1// Not found
2app.get('/users/:id', async (req, res) => {
3 const user = await getUser(req.params.id);
4 if (!user) {
5 return res.notFound('User');
6 // Returns: { success: false, error: 'Not Found', code: 'NOT_FOUND' }
7 }
8 res.success(user);
9});
10
11// Bad request
12app.post('/upload', async (req, res) => {
13 if (!req.files?.file) {
14 return res.badRequest('File is required');
15 // Returns: { success: false, error: 'Bad Request', code: 'BAD_REQUEST' }
16 }
17 res.success({ uploaded: true });
18});
19
20// Unauthorized
21app.get('/profile', async (req, res) => {
22 if (!req.user) {
23 return res.unauthorized('Please log in');
24 // Returns: { success: false, error: 'Unauthorized', code: 'UNAUTHORIZED' }
25 }
26 res.success(req.user);
27});
28
29// Conflict (duplicate)
30app.post('/users', async (req, res) => {
31 const existing = await getUserByEmail(req.body.email);
32 if (existing) {
33 return res.conflict('Email already in use');
34 // Returns: { success: false, error: 'Conflict', code: 'CONFLICT' }
35 }
36 res.created(await createUser(req.body));
37});1app.get('/users')
2 .query(z.object({
3 page: z.coerce.number().min(1).default(1),
4 limit: z.coerce.number().min(1).max(100).default(10)
5 }))
6 .handler(async (req, res) => {
7 const { page, limit } = req.query;
8 const users = await getUsers(page, limit);
9 const total = await getUserCount();
10
11 res.paginated(users, { page, limit, total });
12 // Returns: {
13 // success: true,
14 // data: [...],
15 // pagination: {
16 // page: 1,
17 // limit: 10,
18 // total: 95,
19 // totalPages: 10,
20 // hasNext: true,
21 // hasPrev: false
22 // }
23 // }
24 });Frontend Usage
1// Define the response type
2type ApiResponse<T> =
3 | { success: true; data: T; message?: string }
4 | { success: false; error: string; code?: string; message?: string };
5
6// Use it in your API calls
7async function fetchUser(id: string): Promise<ApiResponse<User>> {
8 const response = await fetch(`/users/${id}`);
9 return await response.json();
10}1const result = await fetchUser('123');
2
3if (result.success) {
4 // ✅ TypeScript knows: result.data exists
5 // ✅ TypeScript knows: result.error does NOT exist
6 console.log(result.data.name); // Fully type-safe!
7} else {
8 // ✅ TypeScript knows: result.error exists
9 // ✅ TypeScript knows: result.data does NOT exist
10 console.log(result.error); // Fully type-safe!
11 // No need to check response.status === 404!
12}Why This Is Better
- No status code checking: Just check
result.success - TypeScript auto-complete: TypeScript knows what properties exist
- Consistent format: All endpoints work the same way
- One handler: Write one function that works for all endpoints
Reference
Success Response
1{
2 success: true, // Always present
3 data: any, // Your response data
4 message?: string // Optional message
5}Error Response
1{
2 success: false, // Always present
3 error: string, // Error name
4 code?: string, // Error code
5 message?: string, // Error message
6 details?: any // Extra details
7}Three Ways to Send Responses
Method 1: Direct res.* Methods (Recommended)
The most intuitive way - use methods directly on the response object. These automatically set the correct HTTP status codes.
1app.get('/users/:id', async (req, res) => {
2 const user = await getUser(req.params.id);
3 if (!user) {
4 return res.notFound('User'); // Automatic 404
5 }
6 res.success(user); // Automatic 200
7});
8
9app.post('/users', async (req, res) => {
10 const user = await createUser(req.body);
11 res.created(user, `/users/${user.id}`); // Automatic 201 + Location header
12});Method 2: Import the response Helper Object
For building response objects (you set the status manually).
1import { response } from '@morojs/moro';
2
3app.get('/users/:id', async (req, res) => {
4 const user = await getUser(req.params.id);
5 if (!user) {
6 return res.status(404).json(response.notFound('User'));
7 }
8 return response.success(user);
9});Method 3: Use ResponseBuilder for Complex Scenarios
For complex scenarios with method chaining.
1import { ResponseBuilder } from '@morojs/moro';
2
3return ResponseBuilder
4 .success(data)
5 .message('Successfully retrieved users')
6 .build();Direct res.* Methods (Recommended)
1// res.success(data, message?) - 200 OK
2app.get('/users', async (req, res) => {
3 const users = await getUsers();
4 res.success(users);
5});
6
7// res.created(data, location?) - 201 Created
8app.post('/users', async (req, res) => {
9 const user = await createUser(req.body);
10 res.created(user, `/users/${user.id}`);
11});
12
13// res.noContent() - 204 No Content
14app.delete('/users/:id', async (req, res) => {
15 await deleteUser(req.params.id);
16 res.noContent();
17});
18
19// res.paginated(data, pagination) - 200 OK with pagination
20app.get('/users', async (req, res) => {
21 const page = parseInt(req.query.page) || 1;
22 const limit = parseInt(req.query.limit) || 10;
23 const users = await getUsers(page, limit);
24 const total = await getUserCount();
25
26 res.paginated(users, { page, limit, total });
27});1// res.badRequest(message?) - 400 Bad Request
2app.post('/upload', async (req, res) => {
3 if (!req.files?.file) {
4 return res.badRequest('File is required');
5 }
6 // ...
7});
8
9// res.unauthorized(message?) - 401 Unauthorized
10app.get('/profile', async (req, res) => {
11 if (!req.user) {
12 return res.unauthorized('Please log in to access your profile');
13 }
14 res.success(req.user);
15});
16
17// res.forbidden(message?) - 403 Forbidden
18app.delete('/users/:id', async (req, res) => {
19 if (!req.user.isAdmin) {
20 return res.forbidden('Only admins can delete users');
21 }
22 // ...
23});
24
25// res.notFound(resource?) - 404 Not Found
26app.get('/users/:id', async (req, res) => {
27 const user = await getUser(req.params.id);
28 if (!user) {
29 return res.notFound('User');
30 }
31 res.success(user);
32});
33
34// res.conflict(message) - 409 Conflict
35app.post('/users', async (req, res) => {
36 const existing = await getUserByEmail(req.body.email);
37 if (existing) {
38 return res.conflict('Email already in use');
39 }
40 // ...
41});
42
43// res.validationError(errors) - 422 Unprocessable Entity
44app.post('/users', async (req, res) => {
45 const errors = [];
46 if (!req.body.email?.includes('@')) {
47 errors.push({
48 field: 'email',
49 message: 'Invalid email format',
50 code: 'INVALID_EMAIL'
51 });
52 }
53 if (errors.length > 0) {
54 return res.validationError(errors);
55 }
56 // ...
57});
58
59// res.rateLimited(retryAfter?) - 429 Too Many Requests
60app.post('/api/send-email', async (req, res) => {
61 const limited = await checkRateLimit(req.ip);
62 if (limited) {
63 return res.rateLimited(60); // Retry after 60 seconds
64 }
65 // ...
66});
67
68// res.internalError(message?) - 500 Internal Server Error
69app.get('/data', async (req, res) => {
70 try {
71 const data = await fetchData();
72 res.success(data);
73 } catch (error) {
74 logger.error('Failed to fetch data', error);
75 res.internalError('Failed to fetch data');
76 }
77});Validation Errors
1app.post('/users', async (req, res) => {
2 const errors = [];
3
4 if (!req.body.name || req.body.name.length < 2) {
5 errors.push({
6 field: 'name',
7 message: 'Name must be at least 2 characters',
8 code: 'MIN_LENGTH'
9 });
10 }
11
12 if (!req.body.email || !req.body.email.includes('@')) {
13 errors.push({
14 field: 'email',
15 message: 'Invalid email format',
16 code: 'INVALID_EMAIL'
17 });
18 }
19
20 if (errors.length > 0) {
21 return res.validationError(errors); // Automatic 422 status
22 }
23
24 const user = await createUser(req.body);
25 res.success(user);
26});
27
28// Response: Status 422 Unprocessable Entity
29// {
30// "success": false,
31// "error": "Validation Failed",
32// "code": "VALIDATION_ERROR",
33// "errors": [
34// {
35// "field": "name",
36// "message": "Name must be at least 2 characters",
37// "code": "MIN_LENGTH"
38// },
39// {
40// "field": "email",
41// "message": "Invalid email format",
42// "code": "INVALID_EMAIL"
43// }
44// ]
45// }1import { z } from '@morojs/moro';
2
3const UserSchema = z.object({
4 name: z.string().min(2),
5 email: z.string().email()
6});
7
8app.post('/users')
9 .body(UserSchema)
10 .handler(async (req, res) => {
11 // Validation is automatic - this only runs if validation passes
12 const user = await createUser(req.body);
13 res.created(user, `/users/${user.id}`);
14 });
15
16// Invalid request automatically returns:
17// Status: 400 Bad Request
18// {
19// "success": false,
20// "error": "Validation failed for body",
21// "details": [ ... validation errors ... ],
22// "requestId": "..."
23// }Pagination
1app.get('/users')
2 .query(z.object({
3 page: z.coerce.number().min(1).default(1),
4 limit: z.coerce.number().min(1).max(100).default(10)
5 }))
6 .handler(async (req, res) => {
7 const { page, limit } = req.query;
8
9 // Get paginated data
10 const users = await getUsers(page, limit);
11 const total = await getUserCount();
12
13 // res.paginated() automatically adds metadata
14 res.paginated(users, { page, limit, total });
15 });
16
17// Response:
18// {
19// "success": true,
20// "data": [ ...users... ],
21// "pagination": {
22// "page": 1,
23// "limit": 10,
24// "total": 95,
25// "totalPages": 10,
26// "hasNext": true,
27// "hasPrev": false
28// }
29// }Pagination Metadata
The pagination object automatically includes:
page- Current page numberlimit- Items per pagetotal- Total number of itemstotalPages- Total number of pages (calculated)hasNext- Boolean indicating if there's a next pagehasPrev- Boolean indicating if there's a previous page
TypeScript Support
1import { response, ApiSuccessResponse, ApiErrorResponse } from '@morojs/moro';
2
3interface User {
4 id: number;
5 name: string;
6 email: string;
7}
8
9app.get('/users/:id', async (req, res) => {
10 const user = await getUser(req.params.id);
11
12 if (!user) {
13 // Type: ApiErrorResponse
14 return res.status(404).json(response.notFound('User'));
15 }
16
17 // Type: ApiSuccessResponse<User>
18 return response.success<User>(user);
19});1import { ApiSuccessResponse, ApiErrorResponse } from '@morojs/moro';
2
3// Function that returns type-safe responses
4async function getUserSafely(id: string): Promise<ApiSuccessResponse<User> | ApiErrorResponse> {
5 const user = await getUser(id);
6
7 if (!user) {
8 return response.notFound('User');
9 }
10
11 return response.success(user);
12}
13
14// Use in route
15app.get('/users/:id', async (req, res) => {
16 const result = await getUserSafely(req.params.id);
17
18 if (!result.success) {
19 return res.status(404).json(result);
20 }
21
22 // TypeScript knows result.data is User here
23 return result;
24});Complete Example
1import { createApp, z } from '@morojs/moro';
2
3const app = await createApp();
4
5// Schemas
6const UserSchema = z.object({
7 name: z.string().min(2).max(50),
8 email: z.string().email(),
9 age: z.number().min(18).optional()
10});
11
12const UpdateUserSchema = UserSchema.partial();
13
14// In-memory storage
15let users = [
16 { id: 1, name: 'John Doe', email: 'john@example.com' },
17 { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
18];
19let nextId = 3;
20
21// GET /users - List all users (with pagination)
22app.get('/users')
23 .query(z.object({
24 page: z.coerce.number().min(1).default(1),
25 limit: z.coerce.number().min(1).max(100).default(10)
26 }))
27 .handler(async (req, res) => {
28 const { page, limit } = req.query;
29 const start = (page - 1) * limit;
30 const paginatedUsers = users.slice(start, start + limit);
31
32 res.paginated(paginatedUsers, {
33 page,
34 limit,
35 total: users.length
36 });
37 });
38
39// GET /users/:id - Get user by ID
40app.get('/users/:id')
41 .params(z.object({ id: z.coerce.number() }))
42 .handler(async (req, res) => {
43 const user = users.find(u => u.id === req.params.id);
44
45 if (!user) {
46 return res.notFound('User');
47 }
48
49 res.success(user);
50 });
51
52// POST /users - Create new user
53app.post('/users')
54 .body(UserSchema)
55 .handler(async (req, res) => {
56 // Check for duplicate email
57 const existing = users.find(u => u.email === req.body.email);
58 if (existing) {
59 return res.conflict('Email already in use');
60 }
61
62 const newUser = {
63 id: nextId++,
64 ...req.body
65 };
66
67 users.push(newUser);
68 res.created(newUser, `/users/${newUser.id}`);
69 });
70
71// PUT /users/:id - Update user
72app.put('/users/:id')
73 .params(z.object({ id: z.coerce.number() }))
74 .body(UpdateUserSchema)
75 .handler(async (req, res) => {
76 const userIndex = users.findIndex(u => u.id === req.params.id);
77
78 if (userIndex === -1) {
79 return res.notFound('User');
80 }
81
82 // Check email uniqueness if being updated
83 if (req.body.email) {
84 const existing = users.find(
85 u => u.email === req.body.email && u.id !== req.params.id
86 );
87 if (existing) {
88 return res.conflict('Email already in use');
89 }
90 }
91
92 users[userIndex] = {
93 ...users[userIndex],
94 ...req.body
95 };
96
97 res.success(users[userIndex], 'User updated successfully');
98 });
99
100// DELETE /users/:id - Delete user
101app.delete('/users/:id')
102 .params(z.object({ id: z.coerce.number() }))
103 .handler(async (req, res) => {
104 const userIndex = users.findIndex(u => u.id === req.params.id);
105
106 if (userIndex === -1) {
107 return res.notFound('User');
108 }
109
110 users.splice(userIndex, 1);
111 res.noContent(); // 204 No Content - no body needed
112 });
113
114app.listen(3000, () => {
115 console.log('Server running on http://localhost:3000');
116});Best Practices
Do
- Use direct res.* methods for simplicity
- Always use standardized responses
- Provide helpful, actionable messages
- Use pagination for lists
- Combine with Zod validation
- Handle errors consistently
- Use res.noContent() for delete operations
Don't
- Mix response formats across endpoints
- Return all items without pagination
- Use generic error messages
- Skip validation for "trusted" inputs
- Forget to set proper status codes
- Return inconsistent error structures
- Ignore TypeScript type safety
All Available Response Methods
Direct res.* Methods (Recommended)
Success responses
res.success(data, message?)- 200 OKres.created(data, location?)- 201 Createdres.noContent()- 204 No Contentres.paginated(data, pagination)- 200 OK with pagination
Error responses
res.badRequest(message?)- 400 Bad Requestres.unauthorized(message?)- 401 Unauthorizedres.forbidden(message?)- 403 Forbiddenres.notFound(resource?)- 404 Not Foundres.conflict(message)- 409 Conflictres.validationError(errors)- 422 Unprocessable Entityres.rateLimited(retryAfter?)- 429 Too Many Requestsres.internalError(message?)- 500 Internal Server Error