DEV Community

Harman Panwar
Harman Panwar

Posted on

Creating Routes and Handling Requests with Express

Express.js: Building Web Servers Without the Boilerplate

Node.js gives you everything you need to create an HTTP server using only its built-in http module. But as your application grows, you find yourself writing the same boilerplate over and over: parsing request bodies, handling routing, managing headers, serving static files. Express.js was created to eliminate this repetition. It is a minimal, unopinionated web framework that sits on top of Node.js and handles the common tasks so you can focus on your application's logic.


What Express.js Is

Express.js is a fast, unopinionated, minimalist web framework for Node.js. It provides a thin layer of fundamental web application features without obscuring Node.js features.

Think of it this way:

  • Node.js gives you the engine, wheels, and chassis of a car
  • Express.js adds the steering wheel, gear stick, and pedals — the controls that make driving practical

You could build a car without a steering wheel (raw Node.js), but every turn would require manually adjusting each wheel. Express gives you a clean interface for the same underlying power.

What Express Provides

Feature Raw Node.js Express.js
Routing Manual URL parsing with if/else chains Declarative app.get(), app.post()
Request body parsing Manual stream collection and JSON parsing Built-in express.json() middleware
Static file serving Manual file reading and streaming express.static() one-liner
Response helpers Manual header setting and res.end() res.send(), res.json(), res.status()
Middleware Manual event handling Clean app.use() pipeline
Error handling Manual try/catch in every route Centralized error middleware

Why Express Simplifies Node.js Development

The Raw Node.js Problem

Here is a raw Node.js server handling two routes and a JSON body:

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
  const parsedUrl = url.parse(req.url, true);

  // Routing: Manual URL checking
  if (parsedUrl.pathname === '/' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Welcome to the homepage');
  } 
  else if (parsedUrl.pathname === '/users' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ users: ['Alice', 'Bob'] }));
  }
  else if (parsedUrl.pathname === '/users' && req.method === 'POST') {
    // Body parsing: Manual stream collection
    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end', () => {
      try {
        const data = JSON.parse(body);
        res.writeHead(201, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ created: data }));
      } catch (e) {
        res.writeHead(400, { 'Content-Type': 'text/plain' });
        res.end('Invalid JSON');
      }
    });
  }
  else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
  }
});

server.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Problems with this approach:

  • Routing requires manual string comparison and method checking
  • JSON body parsing requires stream event handling in every POST route
  • Headers must be set manually for every response
  • Error handling is repetitive and easy to forget
  • Adding a new route means adding another else if block
  • No clean way to add cross-cutting concerns (logging, authentication)

The Express Solution

The same server in Express:

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

app.use(express.json());  // Body parsing middleware — once for all routes

app.get('/', (req, res) => {
  res.send('Welcome to the homepage');
});

app.get('/users', (req, res) => {
  res.json({ users: ['Alice', 'Bob'] });
});

app.post('/users', (req, res) => {
  res.status(201).json({ created: req.body });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
Enter fullscreen mode Exit fullscreen mode

What changed:

  • Routing is declarative: app.get('/') instead of if (pathname === '/' && method === 'GET')
  • Body parsing is centralized: express.json() handles all JSON bodies automatically
  • Response helpers: res.send(), res.json(), res.status() replace manual writeHead and end
  • Error handling: Missing routes automatically return 404
  • Extensibility: New routes are added as new function calls, not nested conditionals

Creating First Express Server

Installation

Express is an npm package. Install it in your project:

mkdir my-express-app
cd my-express-app
npm init -y
npm install express
Enter fullscreen mode Exit fullscreen mode

Minimal Express Server

Create a file named server.js:

const express = require('express');

// Create an Express application
const app = express();

// Define a route
app.get('/', (req, res) => {
  res.send('Hello from Express!');
});

// Start the server
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});
Enter fullscreen mode Exit fullscreen mode

Run it:

node server.js
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:3000 in your browser. You will see Hello from Express!.

Anatomy of an Express App

const express = require('express');  // 1. Import the framework
const app = express();              // 2. Create an application instance

// 3. Add middleware (optional but common)
app.use(express.json());

// 4. Define routes
app.get('/', handler);
app.post('/data', handler);

// 5. Start listening
app.listen(3000, callback);
Enter fullscreen mode Exit fullscreen mode
Step What It Does
require('express') Loads the Express module
express() Creates a new Express application object
app.use() Adds middleware that runs on every request
app.get/post/put/delete() Registers route handlers for specific paths and HTTP methods
app.listen() Binds to a port and starts accepting connections

Handling GET Requests

GET requests are used to retrieve data. In Express, you handle them with app.get().

Basic GET Route

app.get('/about', (req, res) => {
  res.send('This is the about page');
});
Enter fullscreen mode Exit fullscreen mode

When a browser visits http://localhost:3000/about, Express:

  1. Matches the URL /about and method GET
  2. Executes the callback function
  3. Sends the response back to the client

Route Parameters

Capture dynamic values from the URL:

app.get('/users/:id', (req, res) => {
  // req.params contains the route parameters
  const userId = req.params.id;

  res.json({ 
    message: `User profile for ID: ${userId}`,
    requestedAt: new Date().toISOString()
  });
});
Enter fullscreen mode Exit fullscreen mode
URL req.params.id
/users/123 "123"
/users/abc "abc"
/users/99 "99"

Query Strings

Access URL query parameters:

app.get('/search', (req, res) => {
  // req.query contains parsed query string parameters
  const term = req.query.q;
  const limit = req.query.limit || 10;

  res.json({
    searchTerm: term,
    resultsLimit: limit,
    results: [`Result 1 for "${term}"`, `Result 2 for "${term}"`]
  });
});
Enter fullscreen mode Exit fullscreen mode

A request to /search?q=express&limit=5 produces:

{
  "searchTerm": "express",
  "resultsLimit": "5",
  "results": ["Result 1 for \"express\"", "Result 2 for \"express\""]
}
Enter fullscreen mode Exit fullscreen mode

Multiple GET Routes

app.get('/', (req, res) => {
  res.send('Homepage');
});

app.get('/products', (req, res) => {
  res.json([
    { id: 1, name: 'Laptop', price: 999 },
    { id: 2, name: 'Mouse', price: 29 }
  ]);
});

app.get('/products/:id', (req, res) => {
  const productId = parseInt(req.params.id);

  const products = [
    { id: 1, name: 'Laptop', price: 999 },
    { id: 2, name: 'Mouse', price: 29 }
  ];

  const product = products.find(p => p.id === productId);

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

  res.json(product);
});
Enter fullscreen mode Exit fullscreen mode

Handling POST Requests

POST requests are used to create data or submit forms. In Express, you handle them with app.post().

The Body Parsing Problem

By default, Express does not parse request bodies. If you try to access req.body without middleware, it is undefined:

app.post('/users', (req, res) => {
  console.log(req.body);  // undefined!
  res.json({ received: req.body });
});
Enter fullscreen mode Exit fullscreen mode

Adding Body Parsing Middleware

Add this line before your routes:

app.use(express.json());  // Parses JSON bodies
Enter fullscreen mode Exit fullscreen mode

Now req.body contains the parsed JSON object.

Complete POST Example

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

// Middleware: Parse JSON request bodies
app.use(express.json());

// In-memory data store (replaced by a database in real apps)
let users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' }
];
let nextId = 3;

// GET all users
app.get('/users', (req, res) => {
  res.json(users);
});

// GET single user
app.get('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));

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

  res.json(user);
});

// POST: Create new user
app.post('/users', (req, res) => {
  // req.body is now available because of express.json()
  const { name, email } = req.body;

  // Basic validation
  if (!name || !email) {
    return res.status(400).json({ 
      error: 'Name and email are required' 
    });
  }

  const newUser = {
    id: nextId++,
    name,
    email
  };

  users.push(newUser);

  // 201 Created status for successful creation
  res.status(201).json(newUser);
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});
Enter fullscreen mode Exit fullscreen mode

Testing POST with curl

# Create a new user
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Charlie", "email": "charlie@example.com"}'
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "id": 3,
  "name": "Charlie",
  "email": "charlie@example.com"
}
Enter fullscreen mode Exit fullscreen mode

Testing POST with a Form

For HTML form submissions (not JSON), use express.urlencoded():

app.use(express.urlencoded({ extended: true }));

app.post('/contact', (req, res) => {
  // req.body contains form fields: { name: "...", message: "..." }
  res.send(`Thank you, ${req.body.name}. We received your message.`);
});
Enter fullscreen mode Exit fullscreen mode

Sending Responses

Express provides multiple methods for sending responses. Each handles headers and formatting automatically.

Response Methods

Method Use Case Example
res.send() Send a string, buffer, or object (auto-detects type) res.send('Hello')
res.json() Send JSON data (sets Content-Type automatically) res.json({ name: 'John' })
res.status() Set HTTP status code (chainable) res.status(404).send('Not found')
res.redirect() Redirect to another URL res.redirect('/login')
res.sendFile() Send a file from disk res.sendFile('/path/to/file.pdf')

Status Codes

HTTP status codes tell the client what happened:

Code Meaning When to Use
200 OK Successful GET request
201 Created Successful POST request that created a resource
400 Bad Request Client sent invalid data
401 Unauthorized Authentication required
404 Not Found Resource does not exist
500 Internal Server Error Unexpected server error

Chaining Responses

Express response methods are chainable:

// Set status and send JSON in one line
res.status(201).json({ created: true, id: 123 });

// Set status and send text
res.status(404).send('Resource not found');

// Set multiple headers, then send
res.set('X-Custom-Header', 'my-value')
   .status(200)
   .json({ data: 'response' });
Enter fullscreen mode Exit fullscreen mode

Complete Response Examples

// HTML response
app.get('/html', (req, res) => {
  res.send('<h1>Hello World</h1><p>This is HTML</p>');
});

// JSON response
app.get('/json', (req, res) => {
  res.json({ 
    status: 'success',
    timestamp: Date.now(),
    data: [1, 2, 3]
  });
});

// Error response
app.get('/error', (req, res) => {
  res.status(500).json({ 
    error: 'Something went wrong',
    code: 'INTERNAL_ERROR'
  });
});

// Redirect
app.get('/old-page', (req, res) => {
  res.redirect('/new-page');
});
Enter fullscreen mode Exit fullscreen mode

Raw Node.js vs Express: Side by Side

The Same Application, Two Ways

Application requirements:

  • GET / → Returns "Welcome"
  • GET /api/users → Returns JSON array of users
  • POST /api/users → Creates a user from JSON body
  • 404 for unknown routes

Raw Node.js (Verbose)

const http = require('http');
const url = require('url');

let users = [{ id: 1, name: 'Alice' }];
let nextId = 2;

const server = http.createServer((req, res) => {
  const parsed = url.parse(req.url, true);

  // CORS headers (needed for browser access)
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST');

  if (parsed.pathname === '/' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Welcome');
  }
  else if (parsed.pathname === '/api/users' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(users));
  }
  else if (parsed.pathname === '/api/users' && req.method === 'POST') {
    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end', () => {
      try {
        const data = JSON.parse(body);
        const newUser = { id: nextId++, name: data.name };
        users.push(newUser);
        res.writeHead(201, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(newUser));
      } catch (e) {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Invalid JSON' }));
      }
    });
  }
  else if (parsed.pathname === '/api/users' && req.method === 'OPTIONS') {
    res.writeHead(200);
    res.end();
  }
  else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
  }
});

server.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Lines of code: ~50 | Readability: Low | Extensibility: Difficult

Express.js (Concise)

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

let users = [{ id: 1, name: 'Alice' }];
let nextId = 2;

app.use(express.json());

app.get('/', (req, res) => {
  res.send('Welcome');
});

app.get('/api/users', (req, res) => {
  res.json(users);
});

app.post('/api/users', (req, res) => {
  const newUser = { id: nextId++, name: req.body.name };
  users.push(newUser);
  res.status(201).json(newUser);
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Lines of code: ~25 | Readability: High | Extensibility: Easy — just add more app.get() or app.post() calls

Comparison Summary

Aspect Raw Node.js Express.js
Routing Manual string parsing Declarative method + path
Body parsing Stream events per route express.json() once
JSON responses Manual JSON.stringify() res.json() auto-handles
Status codes Manual writeHead() res.status() chainable
404 handling Explicit final else Automatic (if no route matches)
Adding routes Edit central if/else chain Add new function call anywhere
Middleware Manual event hooks app.use() clean pipeline

The Routing Concept

What Is Routing?

Routing determines how an application responds to a client request at a particular endpoint (URI path and HTTP method).

Think of routing like a restaurant menu:

  • Customer asks for "GET /burger" → Kitchen makes a burger
  • Customer asks for "POST /order" → Kitchen records a new order
  • Customer asks for "GET /sushi" → Kitchen says "We don't serve sushi" (404)

Route Structure in Express

app.METHOD(PATH, HANDLER);
Enter fullscreen mode Exit fullscreen mode
Component What It Is Example
app The Express application instance app
METHOD HTTP method in lowercase get, post, put, delete, patch
PATH The URL pattern to match '/', '/users', '/users/:id'
HANDLER Function that executes when route matches (req, res) => {...}

Route Matching Order

Express checks routes in the order they are defined. The first matching route wins:

// This route matches ANY /users/... path
app.get('/users/:id', (req, res) => {
  res.send(`User ${req.params.id}`);
});

// This will NEVER run if placed after the route above
// Because /users/:id matches /users/special first
app.get('/users/special', (req, res) => {
  res.send('Special users page');
});

// Fix: Place specific routes BEFORE generic ones
app.get('/users/special', (req, res) => { ... });  // Specific first
app.get('/users/:id', (req, res) => { ... });      // Generic second
Enter fullscreen mode Exit fullscreen mode

All HTTP Methods

app.get('/items', (req, res) => { ... });      // Retrieve all
app.get('/items/:id', (req, res) => { ... });  // Retrieve one
app.post('/items', (req, res) => { ... });     // Create
app.put('/items/:id', (req, res) => { ... });  // Full update
app.patch('/items/:id', (req, res) => { ... }); // Partial update
app.delete('/items/:id', (req, res) => { ... }); // Delete
Enter fullscreen mode Exit fullscreen mode

Summary

Concept Raw Node.js Express.js
What it is Runtime + built-in http module Web framework built on Node.js
Server creation http.createServer() express() then app.listen()
Routing Manual URL parsing app.get(), app.post(), etc.
Body parsing Stream events express.json() middleware
Response helpers res.writeHead(), res.end() res.send(), res.json(), res.status()
Code volume High (boilerplate-heavy) Low (declarative and concise)

Express does not replace Node.js — it streamlines it. Every Express application is still a Node.js application. The http module is still underneath, handling sockets and protocols. Express simply gives you a cleaner vocabulary for the tasks every web server performs. When you understand what Express abstracts away, you appreciate both the simplicity it offers and the power of Node.js it preserves.

Remember: Raw Node.js is like writing a letter by hand. Express is like using a word processor — the content is still yours, but the formatting, spelling, and structure are handled for you. Both produce letters, but one lets you focus on what you want to say rather than how to align the margins.

Top comments (0)