DEV Community

Cover image for REST API Design Made Simple with Express.js
Pratham
Pratham

Posted on

REST API Design Made Simple with Express.js

How to design clean, predictable APIs that any developer can understand in 5 seconds.


Before I understood REST, my API routes looked like this:

GET  /getUsers
POST /createNewUser
GET  /fetchSingleUser?id=5
POST /deleteUser
GET  /updateUserName
Enter fullscreen mode Exit fullscreen mode

They worked. But they were inconsistent, confusing, and every route had its own naming convention. If another developer looked at my API, they'd have to read every single route to figure out what was going on.

Then I learned REST — a set of principles that tell you exactly how to name routes, which HTTP methods to use, and how to structure your API so it's predictable, clean, and standard. The same routes above became:

GET    /users
POST   /users
GET    /users/5
DELETE /users/5
PUT    /users/5
Enter fullscreen mode Exit fullscreen mode

No verbs in URLs. Consistent patterns. Any developer who knows REST can look at these routes and immediately understand every one of them — without reading a single line of documentation.

That transformation happened for me in the ChaiCode Web Dev Cohort 2026. Let me show you how REST works.


What Does REST API Mean?

Let's break the term down:

API — Application Programming Interface. It's a way for two programs to communicate. Your frontend (React, a mobile app, or even another server) sends a request to the backend, and the backend sends a response.

┌──────────┐    HTTP Request     ┌──────────┐
│          │ ──────────────────→ │          │
│  Client  │                     │  Server  │
│ (React,  │ ←────────────────── │ (Express │
│  mobile) │    HTTP Response    │   API)   │
└──────────┘                     └──────────┘
Enter fullscreen mode Exit fullscreen mode

REST — Representational State Transfer. It's a set of design principles for how APIs should be structured. It's not a technology or framework — it's a style guide.

A REST API is an API that follows REST principles: using standard HTTP methods, organizing data around resources, and communicating with standard status codes.


Resources in REST Architecture

The core idea of REST is simple: everything is a resource.

A resource is any entity your application manages — users, products, orders, comments, posts. Each resource has a URL (endpoint) that represents it.

Resources in a typical app:

  /users        → The collection of all users
  /users/42     → A specific user (ID: 42)
  /products     → The collection of all products
  /products/7   → A specific product (ID: 7)
  /orders       → The collection of all orders
  /orders/101   → A specific order (ID: 101)
Enter fullscreen mode Exit fullscreen mode

REST Naming Conventions

✅ DO:
  /users          ← plural nouns
  /products       ← describes WHAT, not what to DO
  /orders/101     ← specific resource by ID

❌ DON'T:
  /getUsers       ← verb in URL (the HTTP method IS the verb)
  /createProduct  ← action in URL
  /deleteOrder    ← action in URL
  /user           ← singular (use plural for collections)
Enter fullscreen mode Exit fullscreen mode

The golden rule: URLs are nouns, HTTP methods are verbs. The URL tells you what resource. The method tells you what action to perform on it.


HTTP Methods — The Verbs of REST

REST uses standard HTTP methods to perform actions on resources. Each method maps to a CRUD (Create, Read, Update, Delete) operation:

CRUD vs HTTP Methods Mapping

CRUD Operation HTTP Method Express URL What It Does
Create POST app.post() /users Create a new user
Read GET app.get() /users Get all users
Read GET app.get() /users/42 Get a specific user
Update PUT app.put() /users/42 Replace/update a user completely
Delete DELETE app.delete() /users/42 Delete a specific user

The Pattern

Resource: users

  GET    /users       → Get ALL users         (Read — collection)
  GET    /users/:id   → Get ONE user          (Read — single)
  POST   /users       → Create a NEW user     (Create)
  PUT    /users/:id   → Update an EXISTING user (Update)
  DELETE /users/:id   → Delete a user         (Delete)

Same URL pattern, different HTTP methods = different actions.
Enter fullscreen mode Exit fullscreen mode

GET — Reading Data

GET requests retrieve data. They should never modify anything on the server.

// Get all users
app.get("/api/users", (req, res) => {
  res.json(users);
});

// Get a single user
app.get("/api/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);
});
Enter fullscreen mode Exit fullscreen mode

POST — Creating Data

POST requests create new resources. The data comes in the request body.

app.post("/api/users", (req, res) => {
  const { name, email, role } = req.body;

  if (!name || !email) {
    return res.status(400).json({ error: "Name and email are required" });
  }

  const newUser = {
    id: users.length + 1,
    name,
    email,
    role: role || "member",
  };

  users.push(newUser);
  res.status(201).json(newUser);
});
Enter fullscreen mode Exit fullscreen mode

PUT — Updating Data

PUT requests replace an existing resource with new data.

app.put("/api/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" });

  const { name, email, role } = req.body;
  user.name = name || user.name;
  user.email = email || user.email;
  user.role = role || user.role;

  res.json(user);
});
Enter fullscreen mode Exit fullscreen mode

DELETE — Removing Data

DELETE requests remove a resource.

app.delete("/api/users/:id", (req, res) => {
  const index = users.findIndex((u) => u.id === parseInt(req.params.id));
  if (index === -1) return res.status(404).json({ error: "User not found" });

  users.splice(index, 1);
  res.status(204).send(); // 204 = No Content (successfully deleted, nothing to return)
});
Enter fullscreen mode Exit fullscreen mode

Status Codes Basics

HTTP status codes tell the client what happened. They're numbers grouped by category:

1xx — Informational (rarely used directly)
2xx — Success ✅
3xx — Redirection ↗️
4xx — Client Error ❌ (the client did something wrong)
5xx — Server Error 💥 (the server did something wrong)
Enter fullscreen mode Exit fullscreen mode

Status Codes You'll Use Daily

Code Name When to Use Express Example
200 OK Successful GET, PUT res.status(200).json(data)
201 Created Successful POST — new resource created res.status(201).json(user)
204 No Content Successful DELETE — nothing to return res.status(204).send()
400 Bad Request Client sent invalid data res.status(400).json(err)
401 Unauthorized No authentication provided res.status(401).json(err)
403 Forbidden Authenticated but not authorized res.status(403).json(err)
404 Not Found Resource doesn't exist res.status(404).json(err)
500 Internal Server Error Something broke on the server res.status(500).json(err)

Matching Status Codes to Operations

GET    /users      → 200 (here's your data)
GET    /users/999  → 404 (that user doesn't exist)
POST   /users      → 201 (user created!)
POST   /users      → 400 (invalid data — name missing)
PUT    /users/42   → 200 (user updated)
DELETE /users/42   → 204 (deleted, nothing to show)
DELETE /users/999  → 404 (can't delete what doesn't exist)
Enter fullscreen mode Exit fullscreen mode

REST Request-Response Lifecycle

Here's the complete picture of how a REST API request flows through Express:

Client: POST /api/users  { "name": "Pratham", "email": "p@dev.in" }
                │
                ↓
┌──────────────────────────────────────────────────────┐
│                 EXPRESS SERVER                         │
│                                                      │
│  1. Middleware:                                       │
│     express.json() → parses body into req.body       │
│     logger → logs "POST /api/users"                  │
│                                                      │
│  2. Router matches:                                   │
│     app.post("/api/users") ← MATCH ✅                │
│                                                      │
│  3. Route handler runs:                               │
│     → Validates req.body (name and email present?)   │
│     → Creates new user object                        │
│     → Adds to database / array                       │
│     → Sends response                                 │
│                                                      │
│  4. Response:                                         │
│     Status: 201 Created                              │
│     Body: { "id": 4, "name": "Pratham", ... }        │
│                                                      │
└──────────────────────────────────────────────────────┘
                │
                ↓
Client receives: 201 { "id": 4, "name": "Pratham", "email": "p@dev.in" }
Enter fullscreen mode Exit fullscreen mode

Complete Example: Users REST API

Let's build a complete, RESTful Users API from scratch:

const express = require("express");
const app = express();

app.use(express.json());

// In-memory "database"
let users = [
  { id: 1, name: "Pratham", email: "pratham@dev.in", role: "developer" },
  { id: 2, name: "Arjun", email: "arjun@dev.in", role: "designer" },
  { id: 3, name: "Priya", email: "priya@dev.in", role: "manager" },
];

let nextId = 4;

// ── GET /api/users — Get all users ──
app.get("/api/users", (req, res) => {
  // Optional filtering with query params
  let result = [...users];

  if (req.query.role) {
    result = result.filter((u) => u.role === req.query.role);
  }

  res.json({
    count: result.length,
    users: result,
  });
});

// ── GET /api/users/:id — Get single user ──
app.get("/api/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 /api/users — Create new user ──
app.post("/api/users", (req, res) => {
  const { name, email, role } = req.body;

  // Validation
  if (!name || !email) {
    return res.status(400).json({
      error: "Validation failed",
      details: {
        name: !name ? "Name is required" : undefined,
        email: !email ? "Email is required" : undefined,
      },
    });
  }

  // Check for duplicate email
  if (users.find((u) => u.email === email)) {
    return res.status(400).json({ error: "Email already exists" });
  }

  const newUser = {
    id: nextId++,
    name,
    email,
    role: role || "member",
  };

  users.push(newUser);
  res.status(201).json(newUser);
});

// ── PUT /api/users/:id — Update user ──
app.put("/api/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" });
  }

  const { name, email, role } = req.body;
  user.name = name || user.name;
  user.email = email || user.email;
  user.role = role || user.role;

  res.json(user);
});

// ── DELETE /api/users/:id — Delete user ──
app.delete("/api/users/:id", (req, res) => {
  const index = users.findIndex((u) => u.id === parseInt(req.params.id));

  if (index === -1) {
    return res.status(404).json({ error: "User not found" });
  }

  const deleted = users.splice(index, 1)[0];
  res.json({ message: "User deleted", user: deleted });
});

// ── 404 handler — route not found ──
app.use((req, res) => {
  res.status(404).json({ error: `Route ${req.method} ${req.url} not found` });
});

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`REST API running at http://localhost:${PORT}/api/users`);
});
Enter fullscreen mode Exit fullscreen mode

Testing the API

# Get all users
curl http://localhost:3000/api/users

# Get all developers
curl http://localhost:3000/api/users?role=developer

# Get user by ID
curl http://localhost:3000/api/users/1

# Create a new user
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Neha", "email": "neha@dev.in", "role": "tester"}'

# Update a user
curl -X PUT http://localhost:3000/api/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "Pratham Bhardwaj", "role": "senior developer"}'

# Delete a user
curl -X DELETE http://localhost:3000/api/users/2
Enter fullscreen mode Exit fullscreen mode

Route Summary

Method Endpoint Body Response Status
GET /api/users All users 200
GET /api/users/1 Single user 200
GET /api/users/999 Error 404
POST /api/users { name, email, role } Created user 201
POST /api/users { } (missing fields) Validation error 400
PUT /api/users/1 { name, role } Updated user 200
DELETE /api/users/2 Deleted user 200

REST Design Principles — Quick Reference

Principle What It Means
Use nouns in URLs /users, not /getUsers or /createUser
Use plural nouns /users, not /user
HTTP methods = actions GET reads, POST creates, PUT updates, DELETE removes
Use proper status codes 201 for creation, 404 for not found, etc.
Nest related resources /users/42/orders — orders belonging to user 42
Use query for filtering /users?role=admin&sort=name, not /users/role/admin
Be consistent Same patterns across all resources
Stateless Each request contains all info needed — no server memory

Let's Practice: Hands-On Assignment

Part 1: Design REST Routes

Before writing any code, design the routes for a blog API with two resources: posts and comments.

Posts:
  GET    /api/posts          → All posts
  GET    /api/posts/:id      → Single post
  POST   /api/posts          → Create post
  PUT    /api/posts/:id      → Update post
  DELETE /api/posts/:id      → Delete post

Comments (nested under posts):
  GET    /api/posts/:id/comments      → All comments on a post
  POST   /api/posts/:id/comments      → Add comment to a post
  DELETE /api/posts/:postId/comments/:commentId → Delete a comment
Enter fullscreen mode Exit fullscreen mode

Part 2: Build the Posts API

Implement the 5 post routes with validation (title and content required for POST).

Part 3: Add Nested Comments

Add the comment routes. Each comment should reference its parent post's ID.


Key Takeaways

  1. REST is a design style, not a technology. It uses standard HTTP methods as verbs and resource URLs as nouns to create predictable, consistent APIs.
  2. CRUD maps to HTTP methods: Create → POST, Read → GET, Update → PUT, Delete → DELETE. Each action has a specific method — don't use GET for everything.
  3. URLs should be nouns (/users, /products), never verbs (/getUsers). The HTTP method already tells you the action.
  4. Status codes communicate outcomes: 200 for success, 201 for created, 400 for bad input, 404 for not found, 500 for server errors.
  5. Design your routes for one resource at a time (users, then orders, then products). The pattern is always the same — once you've built one RESTful resource, you can build them all.

Wrapping Up

REST isn't complicated — it's a set of conventions that make APIs predictable. Once you know the pattern (nouns for URLs, HTTP methods for actions, status codes for results), you can design any API. And the beauty is that every developer who knows REST can understand your API instantly, without documentation.

I'm learning all of this through the ChaiCode Web Dev Cohort 2026 under Hitesh Chaudhary and Piyush Garg. Building my first proper REST API was the moment backend development stopped being "writing server code" and started being "designing systems." It's a mindset shift, and once it clicks, every API you build will be clean, consistent, and professional.

Connect with me on LinkedIn or visit PrathamDEV.in. More articles on the way.

Happy coding! 🚀


Written by Pratham Bhardwaj | Web Dev Cohort 2026, ChaiCode

Top comments (0)