Express is the most popular Node.js web framework. It provides a thin layer of features on top of Node's built-in http module, making it fast and straightforward to build web applications and APIs.
Installing Express
Start a new project and install Express:
mkdir my-api && cd my-api
npm init -y
npm install express
npm install -D nodemonAdd a dev script to package.json:
{
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js"
}
}Your First Express Server
// src/app.js
const express = require("express");
const app = express();
const PORT = process.env.PORT || 3000;
app.get("/", (req, res) => {
res.json({ message: "Welcome to the API" });
});
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});Run it with npm run dev and visit http://localhost:3000 in your browser.
Understanding Request and Response
Every route handler receives req (request) and res (response) objects.
The Request Object
app.get("/users/:id", (req, res) => {
console.log(req.method); // "GET"
console.log(req.path); // "/users/5"
console.log(req.params.id); // "5"
console.log(req.query); // { sort: "name" } for /users/5?sort=name
console.log(req.headers); // request headers object
console.log(req.body); // request body (needs middleware)
});The Response Object
app.get("/example", (req, res) => {
// JSON response
res.json({ status: "ok" });
// Text response
res.send("Hello, World!");
// Status code + response
res.status(201).json({ created: true });
// Redirect
res.redirect("/new-location");
// Set headers
res.set("X-Custom-Header", "value");
});| Method | Use For |
|---|---|
res.json() | Send JSON data |
res.send() | Send string, buffer, or object |
res.status() | Set HTTP status code |
res.redirect() | Redirect to another URL |
res.set() | Set response headers |
Parsing Request Bodies
Express does not parse request bodies by default. Add the built-in middleware:
const express = require("express");
const app = express();
// Parse JSON bodies
app.use(express.json());
// Parse URL-encoded form data
app.use(express.urlencoded({ extended: true }));
app.post("/users", (req, res) => {
const { name, email } = req.body;
console.log(`Creating user: ${name} (${email})`);
res.status(201).json({ name, email });
});Project Structure
Organize your code as the project grows:
my-api/
├── src/
│ ├── app.js # Express app setup
│ ├── server.js # Server startup
│ ├── routes/
│ │ ├── users.js # User routes
│ │ └── posts.js # Post routes
│ ├── controllers/
│ │ ├── userController.js
│ │ └── postController.js
│ ├── middleware/
│ │ ├── auth.js
│ │ └── errorHandler.js
│ └── config/
│ └── index.js # Configuration
├── package.json
└── .envSeparating App and Server
// src/app.js
const express = require("express");
const app = express();
app.use(express.json());
// Mount routes
app.use("/api/users", require("./routes/users"));
app.use("/api/posts", require("./routes/posts"));
module.exports = app;// src/server.js
const app = require("./app");
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});This separation makes your app easier to test because you can import app without starting the server.
Route Organization
Extract routes into separate files:
// src/routes/users.js
const express = require("express");
const router = express.Router();
const users = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
];
router.get("/", (req, res) => {
res.json(users);
});
router.get("/: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);
});
router.post("/", (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: "Name and email are required" });
}
const newUser = { id: users.length + 1, name, email };
users.push(newUser);
res.status(201).json(newUser);
});
module.exports = router;Configuration Module
Centralize your configuration:
// src/config/index.js
require("dotenv").config();
module.exports = {
port: process.env.PORT || 3000,
nodeEnv: process.env.NODE_ENV || "development",
db: {
host: process.env.DB_HOST || "localhost",
port: process.env.DB_PORT || 5432,
name: process.env.DB_NAME || "myapp",
},
jwtSecret: process.env.JWT_SECRET || "dev-secret",
};Error Handling
Add a global error handler:
// src/middleware/errorHandler.js
function errorHandler(err, req, res, next) {
console.error(err.stack);
const statusCode = err.statusCode || 500;
const message =
process.env.NODE_ENV === "production"
? "Internal Server Error"
: err.message;
res.status(statusCode).json({
error: message,
...(process.env.NODE_ENV !== "production" && { stack: err.stack }),
});
}
module.exports = errorHandler;Register it after all routes:
// src/app.js
const errorHandler = require("./middleware/errorHandler");
// ... routes go here
app.use(errorHandler);Practical Exercise
Set up a complete Express project with proper structure:
// src/app.js
const express = require("express");
const config = require("./config");
const errorHandler = require("./middleware/errorHandler");
const app = express();
app.use(express.json());
// Health check
app.get("/health", (req, res) => {
res.json({
status: "ok",
environment: config.nodeEnv,
uptime: process.uptime(),
});
});
// API routes
app.use("/api/users", require("./routes/users"));
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: `Route ${req.path} not found` });
});
// Error handler
app.use(errorHandler);
module.exports = app;Key Takeaways
- Express adds routing, middleware, and convenience methods on top of Node's
httpmodule. - Use
express.json()middleware to parse JSON request bodies. - Separate your app setup from server startup for testability.
- Organize routes into separate files using
express.Router(). - Always add a global error handler as the last middleware.