So far our servers have only answered questions. Real applications also accept data: signup forms, comments, orders, uploaded settings. That data arrives in the request body, and this lesson shows you how to read it safely with the built-in parsers, build a small in-memory API, and validate what clients send.
HTTP bodies arrive as a raw stream of bytes. Express does not guess the format; you opt in to parsing with middleware. The two built-in parsers cover the common cases:
const express = require('express');
const app = express();
app.use(express.json()); // application/json
app.use(express.urlencoded({ extended: true })); // HTML form postsWith these registered near the top of your file, any request whose Content-Type matches is parsed and the result is placed on req.body. Without them, req.body is undefined, which is the most googled Express beginner error.
Modern frontends send JSON via fetch or axios. Here is a create endpoint for a to-do list:
const todos = [];
let nextId = 1;
app.post('/api/todos', (req, res) => {
const { title } = req.body;
if (!title || typeof title !== 'string' || !title.trim()) {
return res.status(400).json({ error: 'title is required' });
}
const todo = { id: nextId++, title: title.trim(), done: false };
todos.push(todo);
res.status(201).json(todo);
});
app.get('/api/todos', (req, res) => res.json(todos));Two habits to notice: validate before using the body, and answer a successful create with status 201 plus the created object so the client learns the new ID.
You can exercise the endpoint from a terminal:
curl -X POST http://localhost:3000/api/todos \
-H 'Content-Type: application/json' \
-d '{"title": "Learn Express"}'The Content-Type header matters. If you omit it, express.json() skips the body and req.body stays empty. Postman and Insomnia set it for you when you choose the JSON body type.
Classic server-rendered forms send application/x-www-form-urlencoded data. The urlencoded parser turns fields into req.body properties:
<form action="/subscribe" method="POST">
<input name="email" type="email" required>
<button>Subscribe</button>
</form>app.post('/subscribe', (req, res) => {
const email = (req.body.email || '').trim();
if (!email.includes('@')) {
return res.status(400).send('Please provide a valid email.');
}
res.send('Subscribed: ' + email);
});The extended: true option lets the parser understand nested objects like user[name]; for flat forms either setting works.
POST creates, but bodies also power updates. A PATCH endpoint completes the picture:
app.patch('/api/todos/:id', (req, res) => {
const todo = todos.find(t => t.id === Number(req.params.id));
if (!todo) return res.status(404).json({ error: 'Not found' });
if (typeof req.body.done === 'boolean') todo.done = req.body.done;
res.json(todo);
});express.json accepts bodies up to 100kb by default. Raise or lower it deliberately with app.use(express.json({ limit: '10kb' })) to blunt abuse. Also remember that in-memory arrays vanish on restart; they are perfect for learning and prototypes, and lesson 10 replaces them with MongoDB.
Next lesson: Express Router and Project Structure, where we split this growing file into clean modules.