Web Frameworks & Routing - 1/2
From Raw Node.js to Production-Ready APIs
You’ve got Node.js running and understand the fundamentals. You can create a basic HTTP server, handle environment variables, and manage processes. But here’s where most developers hit their second major wall: building a real API with just raw Node.js is like performing surgery with a butter knife.
The raw Node.js reality check:
const http = require("http");
const url = require("url");
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const method = req.method;
const path = parsedUrl.pathname;
// This is already getting messy...
if (method === "GET" && path === "/users") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ users: [] }));
} else if (method === "POST" && path === "/users") {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
// Parse JSON, validate, handle errors... this is getting ridiculous
res.writeHead(201, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "User created" }));
});
} else {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
}
});
After handling 10 endpoints like this, you’ll want to quit programming and become a farmer. This approach doesn’t scale, isn’t maintainable, and will drive you insane trying to handle authentication, validation, error handling, and all the other requirements of real applications.
Web frameworks solve this complexity by providing structured patterns, automated request parsing, middleware systems, and all the tools you need to build APIs without losing your sanity.
We’re focusing on Express.js because it’s the most widely used Node.js framework, has excellent documentation, and teaches patterns you’ll see across the entire web development ecosystem. Master Express, and you’ll understand web frameworks in general.
Express.js: The Foundation of Node.js Web Development
Why Express.js Became the Standard
Express.js isn’t just a framework—it’s the foundation that 90% of Node.js web applications are built on. It provides the perfect balance between simplicity and power, letting you build everything from simple APIs to complex enterprise applications.
What Express.js gives you:
- Routing system: Clean URL handling without manual string parsing
- Middleware architecture: Reusable functions that process requests
- Request/response helpers: JSON parsing, cookie handling, and more built-in
- Template engine support: Server-side rendering when needed
- Static file serving: Automatically serve CSS, images, and other assets
- Extensive ecosystem: Thousands of middleware packages for any functionality
The transformation from raw Node.js to Express:
// Raw Node.js - painful
const server = http.createServer((req, res) => {
// 50 lines of manual request parsing and routing...
});
// Express.js - elegant
const express = require("express");
const app = express();
app.get("/users", (req, res) => {
res.json({ users: [] });
});
app.post("/users", (req, res) => {
res.status(201).json({ message: "User created" });
});
The difference is profound. Express abstracts away the tedious parts while giving you full control over the important business logic.
Setting Up Your First Express Application
Installation and basic setup:
# Initialize your project
npm init -y
# Install Express
npm install express
# Install development dependencies
npm install --save-dev nodemon
Basic Express server (server.js
):
const express = require("express");
const app = express();
const PORT = process.env.PORT || 3000;
// Built-in middleware for parsing JSON
app.use(express.json());
// Basic route
app.get("/", (req, res) => {
res.json({
message: "Welcome to your Express API",
timestamp: new Date().toISOString(),
});
});
// Start the server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Visit: http://localhost:${PORT}`);
});
Package.json scripts:
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
}
Run with npm run dev
for development with auto-restart on file changes.
Express Application Structure
Understanding the Express application object:
const express = require("express");
const app = express(); // This is your application instance
// app.use() - Applies middleware to all routes
app.use(express.json());
// app.get/post/put/delete() - Defines route handlers
app.get("/health", (req, res) => {
res.json({ status: "OK" });
});
// app.listen() - Starts the HTTP server
app.listen(3000);
The app
object is the core of your Express application. Every middleware function, route handler, and configuration option is attached to this object.
Understanding Middleware: The Heart of Express
What Is Middleware?
Middleware functions are the processing pipeline between receiving a request and sending a response. They can:
- Execute code
- Modify request and response objects
- End the request-response cycle
- Call the next middleware function in the stack
Think of middleware like an assembly line:
Request → Middleware 1 → Middleware 2 → Middleware 3 → Response
↓ ↓ ↓
(Logging) (Auth Check) (Route Handler)
Each middleware function receives three parameters:
req
- The request objectres
- The response objectnext
- Function to call the next middleware
Built-in Middleware
Essential built-in middleware you’ll use constantly:
const express = require("express");
const app = express();
// Parse JSON request bodies
app.use(express.json());
// Parse URL-encoded data (form submissions)
app.use(express.urlencoded({ extended: true }));
// Serve static files from 'public' directory
app.use(express.static("public"));
What happens without express.json()
:
app.post("/users", (req, res) => {
console.log(req.body); // undefined - body wasn't parsed!
res.json({ error: "No data received" });
});
With express.json()
middleware:
app.use(express.json());
app.post("/users", (req, res) => {
console.log(req.body); // { name: "Alice", email: "alice@example.com" }
res.json({ message: "User data received", user: req.body });
});
The middleware parsed the JSON request body and made it available in req.body
.
Custom Middleware Functions
Creating your own middleware:
// Simple logging middleware
const logger = (req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${req.method} ${req.url}`);
next(); // IMPORTANT: Call next() to continue to the next middleware
};
// Apply the middleware
app.use(logger);
// Authentication middleware
const requireAuth = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: "Authorization header required" });
}
// In a real app, you'd verify the token here
const token = authHeader.split(" ")[1];
if (token === "valid-token") {
req.user = { id: 1, name: "Alice" }; // Add user info to request
next(); // Continue to the route handler
} else {
res.status(401).json({ error: "Invalid token" });
}
};
// Apply auth middleware to specific routes
app.get("/protected", requireAuth, (req, res) => {
res.json({
message: "This is protected content",
user: req.user,
});
});
Critical middleware concepts:
- Order matters: Middleware executes in the order it’s defined
- Always call
next()
: Unless you’re ending the request-response cycle - Middleware can modify request/response objects: Add properties, transform data
- Error handling: If you don’t call
next()
or send a response, the request will hang
Middleware Execution Patterns
Application-level middleware (applies to all routes):
// Executes for every request
app.use((req, res, next) => {
req.startTime = Date.now();
next();
});
Path-specific middleware:
// Only executes for routes starting with /api
app.use("/api", (req, res, next) => {
console.log("API request received");
next();
});
Route-specific middleware:
// Only executes for this specific route
app.get("/users", requireAuth, (req, res) => {
res.json({ users: [] });
});
Multiple middleware functions:
app.get(
"/admin",
requireAuth, // First: Check authentication
requireAdminRole, // Second: Check admin role
validateRequest, // Third: Validate request data
(req, res) => {
// Finally: Route handler
res.json({ message: "Admin dashboard" });
}
);
Routing: From URLs to Business Logic
Understanding Express Routing
Routing determines how your application responds to different HTTP requests at specific endpoints. Each route consists of:
- HTTP Method (GET, POST, PUT, DELETE, etc.)
- Path Pattern (URL pattern to match)
- Handler Function (code to execute for matched requests)
Basic Route Definitions
HTTP method routing:
const express = require("express");
const app = express();
// GET routes - retrieve data
app.get("/users", (req, res) => {
res.json({ users: [] });
});
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
res.json({ user: { id: userId, name: "Alice" } });
});
// POST routes - create new resources
app.post("/users", (req, res) => {
const userData = req.body;
res.status(201).json({
message: "User created",
user: userData,
});
});
// PUT routes - update/replace resources
app.put("/users/:id", (req, res) => {
const userId = req.params.id;
const updateData = req.body;
res.json({
message: `User ${userId} updated`,
user: updateData,
});
});
// DELETE routes - remove resources
app.delete("/users/:id", (req, res) => {
const userId = req.params.id;
res.json({ message: `User ${userId} deleted` });
});
// PATCH routes - partial updates
app.patch("/users/:id", (req, res) => {
const userId = req.params.id;
const partialUpdate = req.body;
res.json({
message: `User ${userId} partially updated`,
changes: partialUpdate,
});
});
Dynamic Routing with Parameters
Route parameters (:paramName
):
// Single parameter
app.get("/users/:id", (req, res) => {
console.log("User ID:", req.params.id);
res.json({ userId: req.params.id });
});
// Multiple parameters
app.get("/users/:userId/posts/:postId", (req, res) => {
const { userId, postId } = req.params;
res.json({
message: `Post ${postId} by user ${userId}`,
userId,
postId,
});
});
// Optional parameters with ?
app.get("/posts/:year/:month?", (req, res) => {
const { year, month } = req.params;
if (month) {
res.json({ message: `Posts from ${month}/${year}` });
} else {
res.json({ message: `All posts from ${year}` });
}
});
Route parameter validation:
// Custom parameter validation
app.param("id", (req, res, next, id) => {
// Validate that id is a number
if (!/^\d+$/.test(id)) {
return res.status(400).json({ error: "Invalid user ID format" });
}
// Convert to number and attach to request
req.userId = parseInt(id, 10);
next();
});
app.get("/users/:id", (req, res) => {
// req.userId is now guaranteed to be a valid number
res.json({ userId: req.userId });
});
Query String and Request Parsing
Query parameters (?key=value&key2=value2
):
// URL: /search?q=javascript&category=tutorial&page=2
app.get("/search", (req, res) => {
const { q, category, page } = req.query;
res.json({
searchTerm: q,
category: category || "all",
page: parseInt(page) || 1,
allQueryParams: req.query,
});
});
// With default values and type conversion
app.get("/products", (req, res) => {
const {
category = "all",
minPrice = 0,
maxPrice = Infinity,
sortBy = "name",
page = 1,
limit = 10,
} = req.query;
res.json({
filters: {
category,
priceRange: [parseFloat(minPrice), parseFloat(maxPrice)],
sortBy,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
},
},
});
});
Request body parsing (already covered, but important to reinforce):
// Make sure to use these middleware for body parsing
app.use(express.json()); // for application/json
app.use(express.urlencoded({ extended: true })); // for form data
// Now you can access parsed body data
app.post("/users", (req, res) => {
const { name, email, age } = req.body;
// Basic validation
if (!name || !email) {
return res.status(400).json({ error: "Name and email are required" });
}
res.status(201).json({
message: "User created successfully",
user: { name, email, age: age || null },
});
});
Headers and other request properties:
app.get("/request-info", (req, res) => {
res.json({
method: req.method,
url: req.url,
headers: req.headers,
userAgent: req.get("User-Agent"),
contentType: req.get("Content-Type"),
ip: req.ip,
protocol: req.protocol,
secure: req.secure,
});
});
Router Module for Organization
As your application grows, organize routes using Express Router:
// routes/users.js
const express = require("express");
const router = express.Router();
// All routes here are prefixed with /users
router.get("/", (req, res) => {
res.json({ users: [] });
});
router.get("/:id", (req, res) => {
res.json({ user: { id: req.params.id } });
});
router.post("/", (req, res) => {
res.status(201).json({ message: "User created" });
});
module.exports = router;
// server.js
const express = require("express");
const usersRouter = require("./routes/users");
const app = express();
app.use(express.json());
// Mount the users router
app.use("/users", usersRouter);
app.listen(3000);
This pattern scales beautifully:
// server.js - Clean and organized
app.use("/api/users", require("./routes/users"));
app.use("/api/posts", require("./routes/posts"));
app.use("/api/auth", require("./routes/auth"));
app.use("/api/admin", require("./routes/admin"));
Advanced Routing Patterns
Route handlers with multiple functions:
const validateUser = (req, res, next) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: "Missing required fields" });
}
next();
};
const createUser = (req, res) => {
// User validation passed, create the user
res.status(201).json({ message: "User created", user: req.body });
};
// Route with multiple handler functions
app.post("/users", validateUser, createUser);
Catch-all routes:
// Handle all GET requests that haven't been matched
app.get("*", (req, res) => {
res.status(404).json({
error: "Route not found",
path: req.path,
});
});
Route pattern matching:
// Wildcard patterns
app.get("/files/*", (req, res) => {
const filepath = req.params[0]; // Everything after /files/
res.json({ requestedFile: filepath });
});
// Regular expression patterns
app.get(/.*fly$/, (req, res) => {
res.json({ message: 'Routes ending with "fly"' });
});
Response Handling: Sending Data Back to Clients
Express Response Methods
Express provides many helper methods for sending responses:
app.get("/examples", (req, res) => {
// Different ways to send responses
// Send JSON data (most common for APIs)
res.json({ message: "Hello World", data: [] });
// Send plain text
res.send("Hello World");
// Send with specific status code
res.status(201).json({ message: "Created" });
// Send file
res.sendFile("/path/to/file.pdf");
// Redirect to another URL
res.redirect("/new-location");
// Set headers and send
res
.set("Content-Type", "application/json")
.set("Cache-Control", "no-cache")
.json({ data: "with custom headers" });
});
Status Codes and Error Responses
Proper HTTP status codes for different scenarios:
const users = []; // Mock database
app.get("/users/:id", (req, res) => {
const user = users.find((u) => u.id === req.params.id);
if (!user) {
return res.status(404).json({
error: "User not found",
code: "USER_NOT_FOUND",
});
}
res.json({ user }); // 200 OK (default)
});
app.post("/users", (req, res) => {
const { name, email } = req.body;
// Validation error
if (!name || !email) {
return res.status(400).json({
error: "Validation failed",
details: {
name: !name ? "Name is required" : null,
email: !email ? "Email is required" : null,
},
});
}
// Check for existing user
if (users.find((u) => u.email === email)) {
return res.status(409).json({
error: "User already exists",
code: "DUPLICATE_EMAIL",
});
}
const newUser = { id: Date.now(), name, email };
users.push(newUser);
// 201 Created
res.status(201).json({
message: "User created successfully",
user: newUser,
});
});
Response Formatting and Content Types
Consistent API response format:
// Helper function for consistent responses
const sendResponse = (res, statusCode, success, message, data = null) => {
res.status(statusCode).json({
success,
message,
data,
timestamp: new Date().toISOString(),
});
};
// Usage in routes
app.get("/users", (req, res) => {
const users = []; // Get from database
sendResponse(res, 200, true, "Users retrieved successfully", users);
});
app.post("/users", (req, res) => {
try {
// Create user logic...
sendResponse(res, 201, true, "User created", newUser);
} catch (error) {
sendResponse(res, 500, false, "Internal server error");
}
});
Content negotiation based on Accept header:
app.get("/users/:id", (req, res) => {
const user = { id: 1, name: "Alice", email: "alice@example.com" };
res.format({
"text/plain": () => {
res.send(`User: ${user.name} (${user.email})`);
},
"text/html": () => {
res.send(`<h1>${user.name}</h1><p>Email: ${user.email}</p>`);
},
"application/json": () => {
res.json({ user });
},
default: () => {
res.status(406).send("Not Acceptable");
},
});
});
Key Takeaways
Express.js transforms Node.js from a capable runtime into a powerful web development platform. The middleware architecture provides unlimited extensibility while maintaining clean, readable code organization.
The mental shifts you need to make:
- From procedural to middleware thinking: Break complex operations into reusable middleware functions
- From manual parsing to automatic processing: Let Express handle request parsing, routing, and response formatting
- From monolithic handlers to modular routing: Organize code by resource and functionality
What we’ve established:
- Express.js provides the structure and tools for building production-ready APIs
- Middleware functions create processing pipelines for requests and responses
- Routing systems map URLs to business logic with parameter and query string handling
- Proper response handling includes status codes, error formatting, and content negotiation
What’s Next
In the next article, we’ll dive into error handling, CORS configuration, security headers, static file serving, and explore alternative frameworks like Fastify and NestJS. We’ll also cover production considerations and deployment strategies.
The foundation is solid—you understand Express fundamentals, middleware architecture, and routing patterns. Next, we’ll make your applications bulletproof and ready for real-world deployment.
You’re building the skills that separate hobbyist developers from professional backend engineers. Keep the momentum going.