Skip to main content

πŸ—ΊοΈ Basic Routing and Request Handling

Routing is how Express decides which code runs for which URL. In this lesson you'll wire up the four core HTTP methods, capture values from the URL, read query strings, and send back exactly the response β€” and status code β€” the client expects.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Define routes for the GET, POST, PUT, and DELETE methods
  • Capture URL segments with route parameters (req.params)
  • Read optional data from the query string (req.query)
  • Parse a JSON request body with express.json()
  • Send responses with the correct status codes using res.status(), res.json(), and res.send()

Estimated Time: 35–45 minutes  β€’  Difficulty: Beginner

Hands-on: Build a small in-memory "tasks" API that reads and writes data over four HTTP methods.

In This Lesson

What Is a Route?

A route is a rule that pairs an HTTP method and a URL path with a handler function. When a request arrives, Express finds the first route whose method and path match and runs its handler. Every route follows the same shape:

app.METHOD(PATH, HANDLER);
  • app β€” your Express application object
  • METHOD β€” an HTTP method in lowercase: get, post, put, delete
  • PATH β€” the URL path to match, like / or /users/:id
  • HANDLER β€” (req, res) => { ... }, the function that answers the request
πŸ’‘ A useful analogy: Routing is a mail sorting room. Each letter (request) carries an address (path) and a type of delivery (method). The sorter reads both and drops the letter into exactly the right clerk's tray (handler). Get the address or delivery type wrong and it lands in the "return to sender" bin β€” a 404.

πŸ“– Key Terms

req (request): an object describing the incoming request β€” its params, query, body, and headers.

res (response): the object you use to send data back β€” res.json(), res.status(), res.send().

Endpoint: a specific method + path combination, e.g. GET /tasks.

HTTP Methods as Routes

The same path can behave differently depending on the method. By convention, the four core methods map to the four things you do with data β€” often called CRUD (Create, Read, Update, Delete).

MethodIntentExample endpointCRUD
GETRetrieve dataGET /tasksRead
POSTCreate a new resourcePOST /tasksCreate
PUTReplace/update a resourcePUT /tasks/:idUpdate
DELETERemove a resourceDELETE /tasks/:idDelete
// The same path, four different behaviors by method
app.get('/tasks', (req, res) => {
  res.send('List all tasks');
});

app.post('/tasks', (req, res) => {
  res.send('Create a new task');
});

app.put('/tasks/:id', (req, res) => {
  res.send(`Replace task ${req.params.id}`);
});

app.delete('/tasks/:id', (req, res) => {
  res.send(`Delete task ${req.params.id}`);
});
graph TD A[Incoming request] --> B{Method + path} B -->|GET /tasks| C[List tasks] B -->|POST /tasks| D[Create task] B -->|PUT /tasks/:id| E[Update task] B -->|DELETE /tasks/:id| F[Delete task] B -->|No match| G[404 Not Found]

πŸ’‘ Use nouns, not verbs

The method already says what you're doing, so the path should name the thing: prefer POST /tasks over POST /createTask. This is the heart of RESTful design.

Route Parameters

A path segment starting with a colon is a route parameter β€” a placeholder that captures whatever appears in that position. Express puts captured values on req.params.

// :id captures the segment after /tasks/
app.get('/tasks/:id', (req, res) => {
  res.send(`You asked for task ${req.params.id}`);
});
// GET /tasks/42  ->  "You asked for task 42"

// Multiple parameters
app.get('/users/:userId/tasks/:taskId', (req, res) => {
  const { userId, taskId } = req.params;
  res.send(`Task ${taskId} belonging to user ${userId}`);
});
How a route parameter maps a URL to req.params The URL slash tasks slash 42 matches the pattern slash tasks slash colon id, and the value 42 becomes req.params.id. /tasks/:id /tasks/42 req.params.id === "42"
Figure 1 β€” The :id placeholder captures whatever sits in that URL slot and delivers it as req.params.id.

⚠️ Params are always strings

req.params.id is the string "42", not the number 42. Convert it before doing math or comparisons: Number(req.params.id) or parseInt(req.params.id, 10). Forgetting this is a classic first-week bug.

Query Parameters

Everything after the ? in a URL is the query string. Unlike route params (which identify a resource), query params carry options like filtering, sorting, and pagination. Express parses them onto req.query.

// URL: /tasks?status=done&limit=5
app.get('/tasks', (req, res) => {
  const { status, limit } = req.query;
  res.json({
    status: status || 'all',
    limit: Number(limit) || 10
  });
});
// -> { "status": "done", "limit": 5 }
Use caseExample URL
Filtering/tasks?status=done
Pagination/tasks?page=2&limit=20
Sorting/tasks?sortBy=due&order=asc
Searching/tasks?q=report
πŸ’‘ Params vs. query: The route parameter is like the street address on an envelope β€” it decides where the letter goes. The query string is like handling notes scrawled on the outside β€” "fragile", "deliver by noon" β€” extra instructions that don't change the destination.

Reading the Request Body

GET requests carry data in the URL, but POST and PUT usually send it in the request body as JSON. Express doesn't parse bodies by default β€” you must enable the built-in express.json() middleware once, near the top of your app. Then the parsed object appears on req.body.

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

// Enable JSON body parsing for every route (do this once, early)
app.use(express.json());

app.post('/tasks', (req, res) => {
  // req.body is now the parsed JSON object
  const { title } = req.body;
  res.status(201).json({ id: 1, title });
});

⚠️ Forgot express.json()?

Without it, req.body is undefined and destructuring it throws. If your POST handler crashes reading req.body, this missing one-liner is almost always why. For HTML form submissions (URL-encoded), add app.use(express.urlencoded({ extended: true })) too.

Shaping Responses

The res object is how you reply. Pick the method that fits the payload, and always send an appropriate HTTP status code so clients know what happened.

res.send('Plain text or HTML');       // auto-detects content type
res.json({ message: 'Structured' });   // sends JSON + correct header
res.status(201).json({ id: 1 });       // set a status, then send
res.status(404).json({ error: 'Not found' });
res.sendStatus(204);                   // status code with no body
StatusMeaningWhen to use
200 OKSuccessA GET, PUT, or DELETE that worked
201 CreatedResource createdA successful POST
400 Bad RequestClient errorMissing or invalid input
404 Not FoundNo such resourceThe requested ID doesn't exist
500 Server ErrorSomething brokeAn unexpected exception

βœ… Status codes are part of your API

A well-behaved API returns 201 after creating, 404 for a missing item, and 400 for bad input β€” not 200 for everything. Clients rely on the code, not just the body, to react correctly.

Worked Example: A Tasks API

Let's tie every concept together into a small, complete API that stores tasks in memory (an array β€” no database yet). It uses all four methods, route params, the request body, and proper status codes.

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

// In-memory "database"
let tasks = [
  { id: 1, title: 'Learn routing', done: false },
  { id: 2, title: 'Build an API', done: false }
];
let nextId = 3;

// READ all (with optional ?done=true filter)
app.get('/tasks', (req, res) => {
  let result = tasks;
  if (req.query.done !== undefined) {
    const wantDone = req.query.done === 'true';
    result = tasks.filter(t => t.done === wantDone);
  }
  res.json(result);
});

// READ one
app.get('/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === Number(req.params.id));
  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }
  res.json(task);
});

// CREATE
app.post('/tasks', (req, res) => {
  const { title } = req.body;
  if (!title) {
    return res.status(400).json({ error: 'title is required' });
  }
  const task = { id: nextId++, title, done: false };
  tasks.push(task);
  res.status(201).json(task);
});

// UPDATE
app.put('/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === Number(req.params.id));
  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }
  if (req.body.title !== undefined) task.title = req.body.title;
  if (req.body.done !== undefined) task.done = req.body.done;
  res.json(task);
});

// DELETE
app.delete('/tasks/:id', (req, res) => {
  const id = Number(req.params.id);
  const exists = tasks.some(t => t.id === id);
  if (!exists) {
    return res.status(404).json({ error: 'Task not found' });
  }
  tasks = tasks.filter(t => t.id !== id);
  res.sendStatus(204); // success, no body
});

app.listen(3000, () => console.log('Tasks API on http://localhost:3000'));

Try it (using curl):

$ curl http://localhost:3000/tasks
[{"id":1,"title":"Learn routing","done":false}, ...]

$ curl -X POST http://localhost:3000/tasks \
    -H "Content-Type: application/json" \
    -d '{"title":"Write tests"}'
{"id":3,"title":"Write tests","done":false}

Notice the pattern in every handler: find or validate, return an error status early if something's wrong, otherwise do the work and respond. That "guard clause first" shape keeps handlers flat and readable.

Hands-on Exercise

πŸ‹οΈ Build a Books API

Objective: Reproduce the CRUD pattern on your own with a new resource.

Instructions:

  1. Start from a fresh Express app with app.use(express.json()) and an in-memory books array. Each book has id, title, and author.
  2. GET /books β€” return all books. Support an optional ?author= query filter.
  3. GET /books/:id β€” return one book, or 404 if the id doesn't exist. Remember to convert the id to a number.
  4. POST /books β€” create a book; respond 400 if title is missing, otherwise 201 with the new book.
  5. DELETE /books/:id β€” remove a book and respond 204, or 404 if it's not there.
πŸ’‘ Hint

Reuse the exact shape from the Tasks API. For the filter, check if (req.query.author) and use books.filter(b => b.author === req.query.author). Always guard for "not found" with an early return res.status(404)... before doing the real work.

βœ… Sample solution
const express = require('express');
const app = express();
app.use(express.json());

let books = [
  { id: 1, title: 'The Hobbit', author: 'Tolkien' },
  { id: 2, title: 'Dune', author: 'Herbert' }
];
let nextId = 3;

app.get('/books', (req, res) => {
  let result = books;
  if (req.query.author) {
    result = books.filter(b => b.author === req.query.author);
  }
  res.json(result);
});

app.get('/books/:id', (req, res) => {
  const book = books.find(b => b.id === Number(req.params.id));
  if (!book) return res.status(404).json({ error: 'Book not found' });
  res.json(book);
});

app.post('/books', (req, res) => {
  const { title, author } = req.body;
  if (!title) return res.status(400).json({ error: 'title is required' });
  const book = { id: nextId++, title, author };
  books.push(book);
  res.status(201).json(book);
});

app.delete('/books/:id', (req, res) => {
  const id = Number(req.params.id);
  if (!books.some(b => b.id === id)) {
    return res.status(404).json({ error: 'Book not found' });
  }
  books = books.filter(b => b.id !== id);
  res.sendStatus(204);
});

app.listen(3000, () => console.log('Books API on http://localhost:3000'));

Quiz

🎯 Check Your Understanding

Question 1: A request comes in as GET /tasks/7. Inside a handler registered for /tasks/:id, what is req.params.id?

Question 2: Your POST /tasks handler throws when reading req.body.title, and req.body is undefined. What's the most likely fix?

Question 3: Which status code should a successful POST /tasks that creates a new task return?

Summary

πŸŽ‰ Key Takeaways

  • A route is app.METHOD(PATH, HANDLER) β€” matching method + path to a function.
  • GET/POST/PUT/DELETE map to the CRUD operations; name paths with nouns, not verbs.
  • Route params (req.params) identify a resource and are always strings; query params (req.query) carry options.
  • Enable express.json() once so POST/PUT bodies land on req.body.
  • Respond with the right method and status code: 201 on create, 404 when missing, 400 for bad input.

πŸ“š Further Reading

πŸš€ What's Next?

You've been quietly using middleware already (express.json()). Next, Middleware Architecture and Flow unpacks how the middleware pipeline really works β€” the key to logging, authentication, and clean error handling.

πŸŽ‰ You built an API!

Routing and request handling are the backbone of every backend. Onward to middleware.