Sending JSON Data Using Express res.json() Method

An Express API sends data back to the client as text, and JSON (JavaScript Object Notation) is the format the client can turn back into values. The res.json() method converts a JavaScript value to JSON and sends it with the right content type.

This guide shows you how to send JSON from a route and what happens to values like undefined along the way.

What Express res.json() sends

Express res.json() serializes a JavaScript value as JavaScript Object Notation (JSON) text that a client can parse from a Hypertext Transfer Protocol (HTTP) response.

For a JSON endpoint, pass the value directly to res.json() and let Express serialize it with JSON.stringify(). Unless you set another response type beforehand, Express sets Content-Type to application/json and sends the body.

StageExampleWhat changes
Server value{ message: ‘Content to send’ }A JavaScript object holds a string property.
Response body{"message":"Content to send"}JSON text uses double-quoted property names.
Client valuedata.messageA client parses the text and accesses the property.

The Express response reference allows objects and arrays as well as primitive JSON values. Calling res.json() also ends the response and returns the response object, which is why you can chain res.status(400).json(…).

Prerequisites for the local Express server

Use Node.js with npm and a terminal that can run curl. The examples use CommonJS files ending in .cjs, so they work without changing the module type in package.json.

  • Node.js installed. These examples ran on Node.js 26.10.0 with npm 11.19.1.
  • Express installed in the project directory. The unpinned installation used Express 5.2.1.
  • Port 3100 available on your machine and a second terminal for requests.

From an empty project directory, initialize the package and install Express without selecting an older release. The Express installation instructions require Node.js 18 or higher for Express 5.

npm init -y
npm install express
node --version
npm list express

The example binds to 127.0.0.1, so it is reachable only from your own machine. No database or account is required.

Step 1: Create the JSON response route

Save this as app.cjs in the directory where you installed Express. A GET request to /api reads the message variable and places its value in the outgoing object.

const express = require('express');
const app = express();
const port = 3100;

app.get('/api', (req, res) => {
  const message = 'Content to send';
  return res.json({ message });
});

app.listen(port, '127.0.0.1', () => {
  console.log(`Listening at http://127.0.0.1:${port}`);
});

The route callback receives the request and response objects as req and res. The object shorthand { message } means { message: message }, so a variable becomes a named JSON property.

Run the server in your first terminal and keep that process open while sending requests from the second terminal. Stop it with Ctrl+C before changing the file, then start it again.

node app.cjs

The terminal prints Listening at http://127.0.0.1:3100. Open http://127.0.0.1:3100/api in a browser to see the message object.

Browser displaying the JSON message Content to send
The browser pretty-prints the message object. This capture uses port 3000, while the runnable example uses port 3100.

Browser indentation is a display choice, not proof that the server sent indented text. The curl request below shows the compact response body, and you can use a JavaScript object with additional named properties when the client needs more fields.

Step 2: Validate incoming JSON

To receive a message and return it, add this code after the GET route and before app.listen(). The GET response needs no request-body parser, but POST input must be parsed before you read req.body.

app.use(express.json({ limit: '10kb' }));

app.post('/api', (req, res) => {
  if (!req.is('application/json')) {
    return res.status(415).json({ error: 'Send application/json' });
  }

  const message = req.body?.message;
  if (typeof message !== 'string' || message.trim() === '') {
    return res.status(400).json({ error: 'message must be a non-empty string' });
  }

  return res.status(200).json({ message: message.trim() });
});

app.use((err, req, res, next) => {
  if (res.headersSent) return next(err);
  if (err.type === 'entity.parse.failed') {
    return res.status(400).json({ error: 'Malformed JSON' });
  }
  if (err.type === 'entity.too.large') {
    return res.status(413).json({ error: 'JSON body exceeds 10kb' });
  }
  return res.status(500).json({ error: 'Request failed' });
});

The express.json() middleware parses incoming application/json request bodies. res.json() performs the opposite job by serializing an outgoing response.

A body parser does not establish that the data has the shape your route needs. The message check rejects missing or non-string values, and trimming prevents whitespace-only input from passing validation.

Each rejected branch returns a response immediately, so execution cannot fall through to the success response. Calling res.json() alone sends the response but does not stop later JavaScript statements in the handler.

The error middleware comes after the routes, so malformed JSON becomes a JSON error response and an oversized body returns HTTP 413. Non-JSON content types return HTTP 415.

The success branch returns HTTP 200 because it echoes the validated message and stores nothing.

Verify the response body and headers

Restart app.cjs after adding the POST handler. Inspect the HTTP response with curl rather than relying on a browser showing braces.

Check the GET response

Send this request from the second terminal. The -i option includes response headers, while -sS suppresses progress output and still shows curl errors.

curl -sS -i http://127.0.0.1:3100/api

HTTP 200 OK and the application/json content type identify a successful JSON response, whose body contains {"message":"Content to send"}.

Express GET response with HTTP 200 and application/json content type
GET /api returns a JSON object and the application/json response header.

Check accepted and rejected input

Send a message with the request Content-Type set to application/json. The -d argument supplies a body and makes curl use POST.

curl -sS -i http://127.0.0.1:3100/api \
  -H 'Content-Type: application/json' \
  -d '{"message":"Hello"}'

The accepted request returns HTTP 200 and {"message":"Hello"}. Change the body to an empty object to exercise the validation branch rather than the JSON parser.

curl -sS -i http://127.0.0.1:3100/api -H 'Content-Type: application/json' -d '{}'
Express POST response rejecting a missing message with HTTP 400
An empty object reaches validation and returns a JSON error with HTTP 400.

The empty object is valid JSON, but it lacks the message property, so the route returns HTTP 400. If req.body is undefined, inspect the request Content-Type and whether express.json() is registered before the route.

Choose res.json() or res.send() by value

res.send() selects its default content type from the value you pass. It already serializes objects and arrays as JSON, while a string is sent as Hypertext Markup Language (HTML) unless you set a different type.

CallDefault response typeClient receives
res.json({ message: ‘Content to send’ })application/jsonA JSON object
res.send({ message: ‘Content to send’ })application/jsonA JSON object
res.send(‘Content to send’)text/htmlText interpreted as HTML
res.json(‘Content to send’)application/jsonA JSON string, including surrounding quotes
res.json(JSON.stringify(object))application/jsonA JSON string containing encoded object text

I passed the same message object to both methods and got the same JSON body, so I use res.json() to make the route format explicit. Manual JSON.stringify() is unnecessary when the value is already an object.

Add these demonstration routes before the error middleware in app.cjs. The final route deliberately passes encoded text to res.json() so you can see why encoding twice changes the client value.

app.get('/send-text', (req, res) => {
  return res.send('Content to send');
});

app.get('/send-object', (req, res) => {
  return res.send({ message: 'Content to send' });
});

app.get('/double-json', (req, res) => {
  return res.json(JSON.stringify({ message: 'Content to send' }));
});

The /send-text response displays Content to send as page text. The browser capture uses /api on port 3000 for the same string response, rather than the /send-text route used here.

Browser displaying Content to send from an Express string response
A res.send() string appears as text in the browser because this message contains no HTML markup.

With the encoded string, I got a JSON string back instead of an object. Compare these responses after restarting the server.

curl -sS http://127.0.0.1:3100/send-text
curl -sS http://127.0.0.1:3100/send-object
curl -sS http://127.0.0.1:3100/double-json

Pass the original object to res.json(), because parsing /double-json once gives the client a string containing escaped quotes instead of the object it expects.

When JSON serialization changes or rejects data

JSON.stringify() cannot preserve every JavaScript value, and res.json() uses that conversion. Select the response fields deliberately before sending them, especially if the input came from a database or another service.

Value in the outgoing dataSerialization behaviorWhat to do
undefined object propertyThe property is omitted.Use null if an explicit empty field is required.
undefined in an arrayThe entry becomes null.Choose whether the empty position belongs in the response.
NaN or InfinityThe value becomes null.Validate numeric results before sending.
Date objectIts JSON conversion produces a date string.Tell the client the expected date format.
BigInt without a custom conversionSerialization throws TypeError.Convert an identifier to a string to preserve its digits.
Circular object referenceSerialization throws TypeError.Build a response object without the reference cycle.

Save the standalone example as serialization.cjs, then run node serialization.cjs to see omitted properties and caught conversion errors. The BigInt identifier is converted to a string.

const data = {
  message: undefined,
  missing: null,
  created: new Date('2026-10-02T00:00:00Z'),
  id: String(10n)
};
console.log(JSON.stringify(data));
console.log(JSON.stringify([undefined, NaN]));

try {
  JSON.stringify({ id: 10n });
} catch (error) {
  console.log(error.name, error.message);
}

const circular = {};
circular.self = circular;
try {
  JSON.stringify(circular);
} catch (error) {
  console.log(error.name, error.message);
}

The array prints [null,null], and unsupported BigInt and circular-reference conversions each throw a TypeError. The object’s undefined message property is omitted, while missing stays null.

These behaviors follow the JSON.stringify() conversion rules. Constructing an explicit response object also prevents unrelated internal properties from becoming public fields.

An error about headers already being sent means code tried to change the response after it started. Keep one response per route branch, and do not call next() after a completed response unless the remaining middleware is intentionally designed for that state.

Read the message as a client value

Once the endpoint returns an object, the client should parse it once and use the message property. Save this as client.cjs and run node client.cjs while the server is listening.

async function readMessage() {
  const response = await fetch('http://127.0.0.1:3100/api');
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const data = await response.json();
  console.log(data.message);
}

readMessage().catch(error => {
  console.error(error.message);
  process.exitCode = 1;
});

The command prints Content to send. The status check prevents an HTTP error from being treated as a successful message, and response.json() performs the client-side parse that res.json() prepared on the server.

Express JSON response questions

Response format and client parsing are separate choices. These questions address array shape and the helpers whose names can look similar.

Can Express send an array without an object wrapper?

Yes. Pass an array directly to res.json() when the client expects a top-level array, or place it inside an object when the response also needs named metadata fields.

Does res.json() support JSONP callbacks?

Use res.jsonp() for JSON with Padding (JSONP), which wraps the data in a callback. res.json() sends JSON without that callback wrapper.

How can I format a JSON response with indentation?

Set the Express application setting json spaces to an indentation width before sending responses. Browser pretty printing only changes the display and does not establish how the server formatted the body.

Can I await res.json() on the server like response.json() on the client?

Express res.json() returns the response object, not a Promise for parsed data. Fetch response.json() returns a Promise that resolves to the parsed response body.

Pankaj Kumar
Pankaj Kumar

Pankaj Kumar is the founder and CEO of CodeForGeek, with more than 14 years in IT. He is an open-source enthusiast who enjoys sharing what he learns through CodeForGeek and YouTube, with a focus on Python, data analytics, machine learning, Angular, Node.js, and Kafka.

Articles: 337