Learning Node.js - JSON API, CORS, and Response Formats
Episode 10 of 23

Learning Node.js - JSON API, CORS, and Response Formats

This episode designs a consistent JSON API: a uniform payload structure, choosing the right status codes, CORS headers so a frontend can access it, and handling preflight OPTIONS. You test all the headers with curl.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

An API that works isn't necessarily an API that's pleasant to use. The experience of consuming an API is determined by three things: a consistent JSON format, the right status codes, and CORS headers that let a frontend browser access it without obstacles.

Episode 10 refines the Express API from episode 9 toward a standard ready to be consumed by frontends and other consumers. You'll design a uniform response structure, choose status codes deliberately, set up CORS including the preflight OPTIONS, and verify everything with curl.

Designing a Consistent Response Format

Envelope or Plain Data

There are two common styles of JSON response design: sending plain data directly, or wrapping it in an envelope:

JSResponse with an envelope
app.get("/api/artikel", (req, res) => {
  res.json({
    status: "success",
    data: [{ id: 1, judul: "Belajar Node.js" }],
  });
});

An envelope provides a consistent place for metadata such as pagination and error messages. For small projects, plain data is also valid. What matters most: pick one style and stay consistent across all endpoints — don't mix plain data and envelopes in a single API.

Consistency Is a Contract

Consistency becomes the contract between the API and its consumers. If one endpoint returns an envelope and another returns a plain array, consumers have to guess the format per endpoint — a source of unnecessary bugs. Write down this convention in the API documentation and follow it on every route.

The Right Status Codes

Mapping Results to HTTP Codes

Status codes are the universal language between server and client. Make sure every endpoint returns a code that describes the actual result:

JSThe right status codes
app.post("/api/artikel", (req, res) => {
  const artikel = simpanArtikel(req.body);
  res.status(201).json({ status: "success", data: artikel });
});
 
app.get("/api/artikel/:id", (req, res) => {
  const artikel = cariArtikel(req.params.id);
  if (!artikel) {
    res.status(404).json({ status: "error", error: "Artikel tidak ditemukan" });
    return;
  }
  res.json({ status: "success", data: artikel });
});

Notice res.status(201).json(...) for a newly created resource and res.status(404).json(...) for missing data. The same pattern applies to 400 (invalid input), 401 (not logged in), and 403 (no permission), which we'll discuss in episode 11.

CORS and Preflight Requests

Why CORS Is Necessary

Browsers enforce the Same-Origin Policy: JavaScript from http://localhost:5173 can't read responses from http://localhost:3000 without permission. CORS (Cross-Origin Resource Sharing) is that permission mechanism, controlled via response headers.

To choose the allowed origin, add the Access-Control-Allow-Origin header:

JSCORS middleware
app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
  res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
  if (req.method === "OPTIONS") {
    res.sendStatus(204);
    return;
  }
  next();
});

The middleware above allows a specific frontend origin. The value "*" allows all origins — practical for public APIs, but avoid it for APIs with authentication. The Allow-Headers header must include Content-Type and Authorization so the frontend can send JSON and tokens.

Preflight OPTIONS

For non-simple requests — such as POST with Content-Type: application/json or requests with an Authorization header — the browser first sends a preflight OPTIONS request, then waits for permission before the actual request. If the preflight isn't answered with the correct CORS, the real request is cancelled. The if (req.method === "OPTIONS") block above ensures the preflight is answered quickly without being processed as a regular request.

Verifying with curl

Testing Headers from the Terminal

curl -i shows all response headers. Test your CORS with the Origin header:

Test CORS with curl
curl -i -H "Origin: http://localhost:5173" http://localhost:3000/api/artikel
curl -i -X OPTIONS http://localhost:3000/api/artikel \
  -H "Origin: http://localhost:5173" \
  -H "Access-Control-Request-Method: POST"

curl -i -H "Origin: http://localhost:5173" http://localhost:3000/api/artikel must show the Access-Control-Allow-Origin header in the response. The second line simulates the OPTIONS preflight, which must be answered with status 204 and complete CORS headers. If both are correct, your frontend can access the API without issues.

Closing

Here's what to take away:

  • Pick one JSON response style and stay consistent across all endpoints.
  • res.status(201) for new resources and 404 for missing data.
  • Browsers enforce the Same-Origin Policy; CORS grants permission via headers.
  • The Allow-Headers header must include Content-Type and Authorization.
  • The OPTIONS preflight is answered with status 204 and CORS headers.
  • Verify CORS headers with curl -i and the Origin header.

In the next episode, episode 11, we'll discuss basic authentication and authorization — storing passwords with secure hashing, issuing JWT tokens, middleware to verify tokens, and role-based access control. After this, your API has its first security layer.

Learning Node.js - JSON API, CORS, and Response Formats | Learn Node.js