Skip to content

Express API Response Format

Consumers of your API benefit enormously when every endpoint returns responses in a predictable shape. This lesson covers designing and enforcing a consistent JSON response format across an Express API.

Why Response Consistency Matters

Without a shared convention, different routes in the same API often return data in inconsistent shapes, sometimes a bare array, sometimes an object, sometimes a raw error string. This forces every client to write special-case handling for each endpoint.

A simple, consistent envelope, a top-level object with success, data, and error fields, lets client code handle every response the same way, whether it succeeded or failed.

// success
res.status(200).json({
  success: true,
  data: { id: 1, name: 'Ada' },
});

// failure
res.status(404).json({
  success: false,
  error: { message: 'User not found' },
});

Both shapes share the same top-level keys, success and either data or error, making client-side handling predictable.

A Reusable Response Helper

function sendSuccess(res, data, status = 200) {
  res.status(status).json({ success: true, data });
}

function sendError(res, message, status = 400) {
  res.status(status).json({ success: false, error: { message } });
}
  • Centralizing response shaping in small helper functions keeps every route consistent.
  • Attach the actual HTTP status code alongside the same JSON envelope shape.
  • Include pagination metadata (page, limit, total) inside data for collection endpoints.
  • Keep error messages client-safe, never leak stack traces or internal details.

Response Format Cheatsheet

A consistent envelope shape you can reuse across every endpoint.

Scenario Shape
Single resource success { success: true, data: { ... } }
Collection success { success: true, data: [...], meta: { page, limit, total } }
Validation error { success: false, error: { message, fields: [...] } }
Not found { success: false, error: { message: 'Not found' } }
Server error { success: false, error: { message: 'Internal server error' } }

Paginated Collection Example

Collections benefit from a meta block alongside data, so clients can render pagination controls without a separate request.

app.get('/products', (req, res) => {
  const page = Number(req.query.page) || 1;
  const limit = Number(req.query.limit) || 20;
  const start = (page - 1) * limit;
  const items = allProducts.slice(start, start + limit);

  res.json({
    success: true,
    data: items,
    meta: { page, limit, total: allProducts.length },
  });
});

Centralizing With Error-Handling Middleware

Combine a consistent response format with a centralized error handler (from the Error Middleware lesson) so every unhandled error in the app, not just ones you explicitly catch, comes back in the same shape.

app.use((err, req, res, next) => {
  const status = err.status || 500;
  res.status(status).json({
    success: false,
    error: { message: status === 500 ? 'Internal server error' : err.message },
  });
});

Common Mistakes

  • Returning a bare array from some endpoints and a wrapped object from others.
  • Sending different error shapes from different parts of the codebase.
  • Exposing internal error details or stack traces directly in the response body.
  • Forgetting pagination metadata on collection endpoints, forcing clients to guess.

Key Takeaways

  • A consistent JSON envelope, success, data, error, simplifies client-side handling.
  • Reusable response helper functions keep the shape consistent across every route.
  • Collection endpoints should include pagination metadata alongside the data array.
  • A centralized error handler ensures even unexpected errors match the same response shape.

Pro Tip

Document your response envelope shape once, in a shared README or API spec, and treat any endpoint that doesn't match it as a bug, consistency is far easier to maintain than to retrofit.