Project Setup
mkdir my-api && cd my-api
npm init -y
npm install express cors dotenv
npm install --save-dev nodemon
# package.json scripts:
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
}
Create src/index.js as the entry point:
const express = require("express");
const cors = require("cors");
require("dotenv").config();
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json()); // parse JSON request bodies
// Routes
app.get("/", (req, res) => {
res.json({ message: "API is running ✓" });
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Creating Routes
Organize routes in separate files to keep your code clean.
// src/routes/users.js
const express = require("express");
const router = express.Router();
// GET /api/users — get all users
router.get("/", async (req, res) => {
try {
const users = await User.findAll();
res.json(users);
} catch (error) {
res.status(500).json({ error: "Failed to fetch users" });
}
});
// GET /api/users/:id — get one user
router.get("/:id", async (req, res) => {
const { id } = req.params;
const user = await User.findById(id);
if (!user) return res.status(404).json({ error: "User not found" });
res.json(user);
});
// POST /api/users — create user
router.post("/", async (req, res) => {
const { name, email, password } = req.body;
// Validate input
if (!name || !email || !password) {
return res.status(400).json({ error: "All fields required" });
}
const user = await User.create({ name, email, password });
res.status(201).json(user);
});
// PUT /api/users/:id — update user
router.put("/:id", async (req, res) => {
const user = await User.findByIdAndUpdate(req.params.id, req.body);
res.json(user);
});
// DELETE /api/users/:id — delete user
router.delete("/:id", async (req, res) => {
await User.findByIdAndDelete(req.params.id);
res.status(204).send(); // 204 No Content
});
module.exports = router;
Middleware: The Power of Express
Middleware functions run before your route handlers. They can modify requests, check authentication, log activity, or handle errors.
// Custom logging middleware
const logger = (req, res, next) => {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
next(); // MUST call next() to pass to the next middleware/route
};
// Authentication middleware
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ error: "No token" });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch {
res.status(401).json({ error: "Invalid token" });
}
};
// Apply middleware globally
app.use(logger);
// Apply middleware to specific routes only
router.get("/profile", authenticate, (req, res) => {
res.json(req.user);
});
Global Error Handling
// Error handling middleware — MUST have 4 parameters
app.use((err, req, res, next) => {
console.error(err.stack);
const statusCode = err.statusCode || 500;
const message = err.message || "Internal Server Error";
res.status(statusCode).json({
error: message,
...(process.env.NODE_ENV === "development" && { stack: err.stack })
});
});
// Custom error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
// Usage in routes
if (!user) throw new AppError("User not found", 404);
Test Your APIUse Postman or Insomnia to test every endpoint before connecting a frontend. Create a collection with all your routes, including auth headers. This workflow will save you enormous debugging time.