Complete "Lesson 3: Routing, Controllers, and Project Structure" first to unlock this lesson.Go to previous lesson

Introduction

Every useful API needs to receive data from the client — a user ID in the URL, filter options in the query string, or a JSON body with form data. Before you can build CRUD endpoints, you need to know exactly where Express puts each piece of incoming data and how to read it reliably.

This lesson walks through the three main input sources — URL params, query strings, and request bodies — then puts all three to work in a complete set of CRUD endpoints for a simple resource.

Reading Input: Params, Query Strings, and Body

Express parses incoming request data into three distinct objects on the request object.

URL parameters are named segments of the path. You declare them with a colon in your route definition, and Express makes them available on req.params.

javascript
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
res.json({ requested: userId });
});

Query strings appear after the question mark in a URL, like /users?role=admin&limit=10. Express parses them automatically into req.query.

javascript
app.get('/users', (req, res) => {
const { role, limit } = req.query;
res.json({ role, limit });
});

Request bodies carry structured data sent by the client, most often JSON. Express does not parse the body by default — you need the built-in express.json() middleware registered before your routes.

javascript
const express = require('express');
const app = express();
app.use(express.json());

Once that middleware is in place, POST and PUT payloads are available on req.body.

flowchart showing incoming HTTP request splitting into three paths: URL params to req.params, query string to req.query, and JSON body through express.json() middleware to req.body

Building a Full CRUD API

To keep the focus on routing mechanics, the examples below use an in-memory array as the data store. In a real project you would replace the array operations with database calls.

Start with the data and a helper to generate IDs:

javascript
let users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
];
let nextId = 3;

Create — POST /users reads the body and pushes a new record:

javascript
app.post('/users', (req, res) => {
const { name, role } = req.body;
if (!name || !role) {
return res.status(400).json({ error: 'name and role are required' });
}
const newUser = { id: nextId++, name, role };
users.push(newUser);
res.status(201).json(newUser);
});

Read all — GET /users supports optional filtering by role via query string:

javascript
app.get('/users', (req, res) => {
const { role } = req.query;
const result = role ? users.filter(u => u.role === role) : users;
res.json(result);
});

Read one — GET /users/:id uses req.params to find a single record:

javascript
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === Number(req.params.id));
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});

Update — PUT /users/:id combines params (which record) with body (new values):

javascript
app.put('/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === Number(req.params.id));
if (index === -1) return res.status(404).json({ error: 'User not found' });
const { name, role } = req.body;
users[index] = { ...users[index], ...(name && { name }), ...(role && { role }) };
res.json(users[index]);
});

Delete — DELETE /users/:id removes the record and returns 204 No Content:

javascript
app.delete('/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === Number(req.params.id));
if (index === -1) return res.status(404).json({ error: 'User not found' });
users.splice(index, 1);
res.status(204).send();
});
table-style diagram mapping HTTP method and path to CRUD operation: POST /users → Create, GET /users → Read All, GET /users/:id → Read One, PUT /users/:id → Update, DELETE /users/:id → Delete

Common Mistakes

Forgetting express.json() is the most frequent source of confusion for beginners. If req.body is undefined, check that the middleware is registered and that the client is sending a Content-Type: application/json header.

Not converting req.params.id to a number before comparing it is another subtle bug. Route params are always strings. Comparing the string "1" to the number 1 with strict equality will always return false, so your find or findIndex calls will silently miss every record.

Returning data after sending a response causes a "headers already sent" crash in Express. Always use return before res calls inside conditionals, as shown in the examples above.

Summary

Express splits incoming request data across three objects: req.params for URL segments, req.query for query strings, and req.body for the parsed JSON payload. Registering express.json() middleware unlocks body parsing. With those three sources understood, building standard CRUD endpoints is a matter of mapping HTTP methods and status codes to the right data operation — POST and 201 for creation, GET for reads, PUT for updates, DELETE with 204 for removal.

Lesson practice

Lesson Checkpoint

Review what you learned and get feedback on your work.

0/1
Answered · 0/1