Creating Routes and Handling Requests with Express
A clear, practical guide to building your first Express server — from zero to handling real requests.
Every web application you've ever used is built on one fundamental idea: a client asks for something, and a server responds. You type a URL, press Enter, and somewhere a machine receives that request, figures out what you want, and sends something back. That exchange — request and response — is the heartbeat of the web.
Node.js lets you build that server using pure JavaScript. But doing it without any help is surprisingly tedious. Express.js exists to remove that tedium. It doesn't change what your server does — it just makes it dramatically easier to write, read, and maintain.
This blog walks you through everything: what Express is, why it matters, and how to build a server that handles real requests from scratch.
What Express.js Actually Is
Express is a minimal web framework for Node.js. The word minimal is important here — Express doesn't make decisions for you, doesn't force a project structure, and doesn't come bundled with a database layer or a templating engine. It gives you exactly what you need to build a web server and nothing more.
It was built to solve a specific frustration: writing a Node.js HTTP server from scratch is repetitive and verbose. Parsing request bodies, reading URL parameters, setting response headers, routing different URLs to different logic — all of this requires boilerplate code that you'd have to write from scratch, every single time.
Express wraps all of that complexity in a clean, readable API. It sits on top of Node's built-in http module and gives you tools like app.get(), app.post(), req.params, req.body, and res.json() — so you can focus on your application logic instead of HTTP mechanics.
Raw Node.js vs Express — Feel the Difference
The best way to appreciate Express is to see what you'd have to do without it.
Here's a simple Node.js HTTP server that responds with "Hello World":
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World');
} else if (req.url === '/about' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('About page');
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
}
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
This works. But notice what's happening: you're manually checking req.url and req.method for every route. You're manually setting headers every single time. As your app grows, this single createServer callback becomes an unmanageable mess of nested if statements.
Now here's the same thing in Express:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World');
});
app.get('/about', (req, res) => {
res.send('About page');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Each route is its own clean, self-contained block. Adding a new route is one more app.get() call — not another branch in an ever-growing if-else chain. The method matching, header setting, and response formatting are all handled for you.
That's the entire value proposition of Express in one comparison.
Setting Up Your First Express Server
Before writing any code, you need to install Express. In your project folder, run:
npm init -y
npm install express
npm init -y creates a package.json file with default settings. npm install express downloads Express and adds it to your project's dependencies.
Now create a file called server.js and write this:
const express = require('express');
const app = express();
app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});
Three lines. That's a fully functional Express server. Run it with node server.js, open your browser, and navigate to http://localhost:3000. It doesn't respond to anything yet — but it's alive, listening, waiting for requests.
require('express') loads the Express library. Calling express() creates an application instance — this app object is what you'll use to define all your routes and configure your server. app.listen(3000, callback) tells Node to start listening for incoming connections on port 3000 and calls the callback once the server is ready.
Understanding Routes
A route is the combination of two things: a URL path and an HTTP method. Together they define what request your server knows how to handle.
When someone visits https://yourapp.com/users, that's a GET request to the path /users. When a form submits data to https://yourapp.com/users, that's typically a POST request to /users. Same URL, different method, completely different intent — and Express lets you handle each one separately with its own logic.
The general pattern for defining any route in Express is:
app.METHOD(PATH, HANDLER);
Where METHOD is the HTTP method in lowercase (get, post, put, delete), PATH is the URL string, and HANDLER is the function that runs when that route is matched.
The handler function always receives two arguments: req (the request object, containing everything the client sent) and res (the response object, which you use to send something back).
Handling GET Requests
GET requests are what browsers make when you navigate to a URL. They're used to retrieve data — a webpage, a list of users, a product, an article. GET requests should never modify anything on the server. They are read-only.
Here's how you define GET routes in Express:
const express = require('express');
const app = express();
// Home route
app.get('/', (req, res) => {
res.send('Welcome to the homepage!');
});
// About route
app.get('/about', (req, res) => {
res.send('This is the about page.');
});
// API route returning JSON
app.get('/api/users', (req, res) => {
const users = [
{ id: 1, name: 'Rahul', city: 'Mumbai' },
{ id: 2, name: 'Priya', city: 'Delhi' },
{ id: 3, name: 'Ananya', city: 'Pune' }
];
res.json(users);
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Visit http://localhost:3000/ in your browser and you see the homepage message. Visit http://localhost:3000/api/users and you get back a clean JSON array of users.
res.send() sends a plain text or HTML response. res.json() sends a JSON response and automatically sets the Content-Type header to application/json — no manual header setting required.
Route Parameters — Dynamic URLs
Most real applications need URLs that contain variable data — like /users/1 to get user with ID 1, or /products/42 to get product 42. Express handles this with route parameters, which are defined using a colon prefix:
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
res.send(`You requested user with ID: ${userId}`);
});
The :id part is a named placeholder. Whatever appears in the URL at that position gets captured and stored in req.params.id. Visit /users/5 and req.params.id is "5". Visit /users/rahul and req.params.id is "rahul". You can have multiple params in one route — /posts/:postId/comments/:commentId gives you both req.params.postId and req.params.commentId.
Query Parameters — Optional Filters
Sometimes you want to pass optional data in a URL, like search terms or filters. These appear after a ? in the URL: /products?category=shoes&sort=price. Express captures these automatically in req.query:
app.get('/products', (req, res) => {
const category = req.query.category;
const sort = req.query.sort;
res.json({
message: 'Fetching products',
filters: { category, sort }
});
});
Visit /products?category=shoes&sort=price and the response is:
{
"message": "Fetching products",
"filters": { "category": "shoes", "sort": "price" }
}
No parsing. No splitting strings. Express extracts query parameters and hands them to you as a plain object.
Handling POST Requests
POST requests are used to send data to the server — submitting a form, creating a new user, placing an order. Unlike GET, POST requests carry a body: a chunk of data attached to the request that contains whatever the client is sending.
To read that body in Express, you first need to add a middleware that parses incoming JSON:
app.use(express.json());
This one line tells Express: "Before any route handler runs, check if the incoming request has a JSON body. If it does, parse it and make it available on req.body."
app.use() is how you add middleware to Express. Middleware is any function that runs between receiving a request and sending a response — think of it as a pipeline. express.json() is built-in middleware that ships with Express. You just need to activate it.
Now here's a full POST route:
const express = require('express');
const app = express();
app.use(express.json()); // parse incoming JSON bodies
app.post('/users', (req, res) => {
const { name, age, city } = req.body;
if (!name || !age || !city) {
return res.status(400).json({ error: 'Name, age, and city are required.' });
}
const newUser = {
id: Date.now(),
name,
age,
city
};
res.status(201).json({
message: 'User created successfully!',
user: newUser
});
});
app.listen(3000);
When a client sends a POST request to /users with a JSON body like:
{
"name": "Rahul",
"age": 20,
"city": "Mumbai"
}
The handler reads req.body, validates that the required fields are present, and responds with the newly created user. If a required field is missing, it sends back a 400 Bad Request error.
res.status(201) sets the HTTP status code before sending the response. 201 Created is the conventional status for a successful resource creation. 400 Bad Request tells the client their input was invalid. These status codes are not just conventions — they're how clients, browsers, and other servers understand what happened.
Sending Responses — The Full Picture
Express gives you several ways to send responses depending on what you're returning:
// Send plain text or HTML
res.send('Hello World');
// Send JSON — automatically sets Content-Type: application/json
res.json({ message: 'Success', data: [...] });
// Set a status code before sending
res.status(404).json({ error: 'User not found' });
res.status(201).json({ message: 'Created' });
res.status(500).json({ error: 'Something went wrong' });
// Redirect to another route
res.redirect('/login');
One rule to live by: always send exactly one response per request. Calling res.json() and then res.send() in the same handler will throw an error — the response has already been sent, and you can't send it again. Use return to exit the handler early after sending a response:
app.get('/user/:id', (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ error: 'ID must be a number' });
// execution stops here if id is invalid
}
// only reaches here if id is valid
res.json({ id, name: 'Rahul' });
});
Putting It All Together — A Complete Mini Server
Here's a self-contained Express server that demonstrates everything covered in this blog:
const express = require('express');
const app = express();
app.use(express.json());
// In-memory "database" (just an array for now)
let users = [
{ id: 1, name: 'Rahul', age: 20, city: 'Mumbai' },
{ id: 2, name: 'Priya', age: 22, city: 'Delhi' }
];
// GET all users
app.get('/users', (req, res) => {
res.json(users);
});
// GET a single user by ID
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 a new user
app.post('/users', (req, res) => {
const { name, age, city } = req.body;
if (!name || !age || !city) {
return res.status(400).json({ error: 'All fields are required' });
}
const newUser = {
id: users.length + 1,
name,
age,
city
};
users.push(newUser);
res.status(201).json({
message: 'User created!',
user: newUser
});
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
This is a real, working REST API. It has three routes, validation, proper status codes, and clear separation between each piece of logic. You can test it with a tool like Postman or Thunder Client — send a GET to /users, send a GET to /users/1, and send a POST to /users with a JSON body.
How Express Thinks About Requests
Here's the mental model that ties everything together. Every incoming request goes through Express in a linear sequence. Express looks at the request's URL and method, then scans through your defined routes from top to bottom. The first route that matches both the URL and the method gets to handle the request. If no route matches, Express falls through to the end with no response — which is why adding a catch-all route for unmatched URLs is good practice:
// This goes at the very bottom, after all other routes
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
Because Express scans top to bottom, order matters. More specific routes should always come before more generic ones. That's a rule worth remembering the first time a catch-all accidentally captures something it shouldn't.
Summary
Express takes Node's raw HTTP capabilities and wraps them in an API that's readable, predictable, and easy to extend.
express() creates your application instance. app.listen() starts the server. app.get() and app.post() define routes for specific HTTP methods. req.params gives you URL parameters like :id. req.query gives you query string values like ?sort=price. req.body gives you POST request data — after adding app.use(express.json()). res.send() and res.json() send responses back. res.status() sets the HTTP status code.
That's the foundation. Everything else in the Express ecosystem — middleware, authentication, file uploads, database connections — is built on top of these same primitives.
Once you're comfortable with routes and request handling, you're not just writing scripts anymore. You're building servers.
Enjoyed this? Follow for more backend JavaScript concepts explained clearly. Next up: Middleware in Express — the invisible layer that runs before every request reaches your route handler.

