Beginner25 min read

Routes & HTTP Methods

Define Express routes for GET, POST, PUT, and DELETE requests with route parameters and query strings.

What is Routing?

Routing determines how your application responds to client requests at specific endpoints. An endpoint is a combination of a URL path and an HTTP method. For example, GET /users and POST /users are two different endpoints, even though they share the same URL path — one retrieves users, the other creates a new user.

Express provides a method for each HTTP verb (also called HTTP method):

  • app.get(path, handler) — Handle GET requests. Used to retrieve data. When someone visits a URL in their browser, that is a GET request. GET requests should never modify data on the server — they are read-only.
  • app.post(path, handler) — Handle POST requests. Used to create new resources. When a form is submitted or a client sends data to save, that is typically a POST request.
  • app.put(path, handler) — Handle PUT requests. Used to update/replace an existing resource entirely. If you update a user's profile, you send the complete new profile data.
  • app.patch(path, handler) — Handle PATCH requests. Used to partially update a resource. Instead of sending the entire object, you send only the fields that changed.
  • app.delete(path, handler) — Handle DELETE requests. Used to remove a resource.

Each route definition has two parts: a path (the URL pattern to match) and a handler function (the code that runs when a request matches). The handler receives req and res — the same request and response objects from the http module, but enhanced with Express's helper methods.

This combination of HTTP method + path is called a RESTful pattern. REST (Representational State Transfer) is an architectural style for designing APIs. In REST, URLs represent resources (nouns like /users, /products, /orders), and HTTP methods represent actions (verbs like GET, POST, PUT, DELETE). This makes your API intuitive and predictable:

MethodPathAction
GET/usersList all users
GET/users/5Get user with ID 5
POST/usersCreate a new user
PUT/users/5Update user 5
DELETE/users/5Delete user 5

Route Parameters

Route parameters are dynamic segments in your URL paths, prefixed with a colon (:). They let you define a single route that matches many different URLs. This is essential for working with specific resources — you do not want to create a separate route for every single user, product, or post in your database.

Here is a route with a parameter:

javascript
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  res.json({ message: `Fetching user ${userId}` });
});

When a request comes in for GET /users/123, Express matches this route and populates req.params with { id: "123" }. When a request comes in for GET /users/456, the same route handles it with req.params.id equal to "456".

You can have multiple parameters in a single route:

javascript
app.get('/posts/:postId/comments/:commentId', (req, res) => {
  const { postId, commentId } = req.params;
  res.json({ postId, commentId });
});

A request to GET /posts/42/comments/7 gives you req.params = { postId: "42", commentId: "7" }.

Important: parameters are always strings. Even though 123 looks like a number, req.params.id is the string "123". If you need to use it as a number (for example, to look up a database record by numeric ID), you must convert it:

javascript
const userId = parseInt(req.params.id, 10);
// Or:
const userId = Number(req.params.id);

Always validate parameters before using them. A client could send /users/abc or /users/-1 — your code should handle unexpected values gracefully rather than crashing or returning confusing errors.

Route parameters are the backbone of REST APIs. Every time you need to work with a specific resource — get one user, update one product, delete one comment — you use a route parameter to identify which one.

Query Strings

Query strings are key-value pairs appended to the URL after a ? character. They are used for optional information that modifies the request — filtering, sorting, searching, and pagination are the most common use cases.

Example URL with a query string:

/api/products?category=electronics&sort=price&page=2

Express automatically parses query strings and makes them available on req.query:

javascript
app.get('/api/products', (req, res) => {
  console.log(req.query);
  // { category: 'electronics', sort: 'price', page: '2' }

  const { category, sort, page } = req.query;
  // Use these to filter and sort your data
});

Key differences between route parameters and query strings:

FeatureRoute ParamsQuery Strings
Syntax/users/:id/users?role=admin
Accessreq.paramsreq.query
Required?Yes (route won't match without it)No (optional, route matches with or without)
PurposeIdentify a specific resourceModify/filter the request
Example/users/42 (get user 42)/users?sort=name (list users, sorted by name)

Query strings are optional — the route /api/products still matches even if no query parameters are provided. Your code should handle the case where query parameters are missing:

javascript
app.get('/api/products', (req, res) => {
  const page = parseInt(req.query.page) || 1;  // Default to page 1
  const limit = parseInt(req.query.limit) || 10; // Default to 10 items
  const sort = req.query.sort || 'createdAt';    // Default sort field
  // ... use these values to query your database
});

Like route parameters, query string values are always strings. The page in ?page=2 is the string "2", not the number 2. Always convert to the appropriate type before using them in calculations or database queries.

A common pattern is to combine route parameters and query strings: GET /users/42/posts?page=3&limit=5 — get page 3 of posts for user 42, showing 5 per page.

Complete CRUD Routing Example

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

// In-memory data store (in real apps, this would be a database)
let books = [
  { id: 1, title: '1984', author: 'George Orwell', year: 1949 },
  { id: 2, title: 'Dune', author: 'Frank Herbert', year: 1965 },
  { id: 3, title: 'Neuromancer', author: 'William Gibson', year: 1984 },
];
let nextId = 4;

// GET /api/books — List all books (with optional filtering)
app.get('/api/books', (req, res) => {
  let result = books;

  // Filter by author if query param provided: /api/books?author=Orwell
  if (req.query.author) {
    result = result.filter(b =>
      b.author.toLowerCase().includes(req.query.author.toLowerCase())
    );
  }

  // Sort by year if requested: /api/books?sort=year
  if (req.query.sort === 'year') {
    result = [...result].sort((a, b) => a.year - b.year);
  }

  res.json(result);
});

// GET /api/books/:id — Get a single book by ID
app.get('/api/books/:id', (req, res) => {
  const id = parseInt(req.params.id, 10);
  const book = books.find(b => b.id === id);

  if (!book) {
    return res.status(404).json({ error: 'Book not found' });
  }

  res.json(book);
});

// POST /api/books — Create a new book
app.post('/api/books', (req, res) => {
  const { title, author, year } = req.body;
  const newBook = { id: nextId++, title, author, year };
  books.push(newBook);
  res.status(201).json(newBook);
});

// PUT /api/books/:id — Update a book entirely
app.put('/api/books/:id', (req, res) => {
  const id = parseInt(req.params.id, 10);
  const index = books.findIndex(b => b.id === id);

  if (index === -1) {
    return res.status(404).json({ error: 'Book not found' });
  }

  books[index] = { id, ...req.body };
  res.json(books[index]);
});

// DELETE /api/books/:id — Delete a book
app.delete('/api/books/:id', (req, res) => {
  const id = parseInt(req.params.id, 10);
  const index = books.findIndex(b => b.id === id);

  if (index === -1) {
    return res.status(404).json({ error: 'Book not found' });
  }

  books.splice(index, 1);
  res.status(204).end(); // 204 No Content — successful deletion, no body
});

app.listen(3000, () => console.log('Book API running on port 3000'));

Route Organization with express.Router()

As your application grows, putting all routes in a single file becomes messy. Imagine having 50 routes for users, products, orders, reviews, and authentication all in one file — it would be hundreds of lines long and impossible to navigate.

Express solves this with express.Router(). A Router is like a mini Express application — it can have its own routes and middleware, but it does not stand on its own. You create it in a separate file and then mount it in your main app.

Step 1: Create a route file (routes/users.js):

javascript
const express = require('express');
const router = express.Router();

// These routes are relative — '/' here means '/api/users'
router.get('/', (req, res) => {
  res.json([{ id: 1, name: 'Alice' }]);
});

router.get('/:id', (req, res) => {
  res.json({ id: req.params.id, name: 'Alice' });
});

router.post('/', (req, res) => {
  res.status(201).json({ id: 2, name: req.body.name });
});

module.exports = router;

Step 2: Mount the router in your main app (server.js):

javascript
const express = require('express');
const userRouter = require('./routes/users');
const productRouter = require('./routes/products');

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

// Mount routers with a prefix
app.use('/api/users', userRouter);
app.use('/api/products', productRouter);

app.listen(3000);

Now GET /api/users is handled by the users router, and GET /api/products is handled by the products router. Each router is in its own file, keeping your code modular and organized.

The prefix you pass to app.use() is prepended to all routes defined in that router. So when the users router defines router.get('/:id', ...), the actual URL is /api/users/:id. The router does not know or care about its prefix — it only defines the relative paths.

A typical project structure might look like:

html
my-api/
  server.js
  routes/
    users.js
    products.js
    orders.js
    auth.js

This modular approach is standard in professional Express applications. It keeps each file focused on one resource and makes your codebase easier to navigate and maintain.

How do you access a URL parameter named 'id' in Express?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.