Features
Docs
CLI
Benchmarks
Examples

© 2024 MoroJs

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.

We Created a Standard

Every API response includes a success boolean.
Simple, consistent, and type-safe.

The Standard Format

Success
1{
2  success: true,
3  data: {...}
4}
Error
1{
2  success: false,
3  error: '...',
4  code: '...'
5}
With Pagination
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}
Validation Errors
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—that's what makes it work.

Why We Created This Standard

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

It's This Easy

Use simple helper methods. That's it.

Backend - Just use the helpers

typescript

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});

Why It Makes Sense

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, then data exists
  • If success: false, then error exists
  • No need to check HTTP status codes—just check result.success

Common Patterns

Success Responses

typescript

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});

Error Responses

typescript

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});

Pagination

typescript

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

TypeScript Setup

typescript

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}

Using Responses - TypeScript Knows Everything

typescript

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.

Direct res.* Methods

typescript

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).

Using response Helper

typescript

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.

Using ResponseBuilder

typescript

1import { ResponseBuilder } from '@morojs/moro';
2
3return ResponseBuilder
4  .success(data)
5  .message('Successfully retrieved users')
6  .build();

Direct res.* Methods (Recommended)

Success Methods

typescript

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});

Error Methods

typescript

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

Manual Validation with res.validationError()

typescript

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// }

With Zod Validation (Automatic)

typescript

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

Using res.paginated()

typescript

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 number
  • limit - Items per page
  • total - Total number of items
  • totalPages - Total number of pages (calculated)
  • hasNext - Boolean indicating if there's a next page
  • hasPrev - Boolean indicating if there's a previous page

TypeScript Support

Full Type Inference

typescript

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});

Type-Safe Response Functions

typescript

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

Complete REST API with Standardized Responses

typescript

1import { createApp, z } from '@morojs/moro';
2
3const app = 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 OK
  • res.created(data, location?) - 201 Created
  • res.noContent() - 204 No Content
  • res.paginated(data, pagination) - 200 OK with pagination

Error responses

  • res.badRequest(message?) - 400 Bad Request
  • res.unauthorized(message?) - 401 Unauthorized
  • res.forbidden(message?) - 403 Forbidden
  • res.notFound(resource?) - 404 Not Found
  • res.conflict(message) - 409 Conflict
  • res.validationError(errors) - 422 Unprocessable Entity
  • res.rateLimited(retryAfter?) - 429 Too Many Requests
  • res.internalError(message?) - 500 Internal Server Error

Next Steps