DevRoadmap
Backend

How to Build a REST API with Node.js and Express

Building a REST API from scratch is the most important backend skill a full-stack developer can have. This step-by-step guide takes you from an empty folder to a fully working, authenticated API.

READ TIME 13 min read
CATEGORY Backend
Advertisement

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.
Advertisement