# REST API Design Made Simple with Express.js

I built my first API by just creating random routes until something worked. `/getUserData`, `/addNewUser`, `/user-delete`, `/update_user_info`. The endpoints were inconsistent, the HTTP methods were wrong (everything was POST because that is what I knew), and the status codes were mostly just 200 or 500.

Then someone on a code review told me to learn REST. I read about it, rebuilt the API following REST principles, and the difference was obvious. The routes were predictable. The methods made sense. The entire API felt organized instead of chaotic. REST is not complicated. It is just a set of conventions that make APIs consistent and intuitive.

* * *

## What an API Actually Is

An API (Application Programming Interface) is how two programs talk to each other. When you use a mobile app to check the weather, the app does not have the weather data itself. It makes an HTTP request to a weather API, and the API responds with the data in a structured format like JSON.

```plaintext
Mobile App → HTTP Request → Weather API → HTTP Response → Mobile App
```

The app is the client. The API is the server. REST is a set of design principles for building these HTTP-based APIs.

* * *

## What REST Means

REST stands for Representational State Transfer. That definition is not helpful. Here is what it actually means in practice:

REST is an architectural style where:

*   The API exposes **resources** (things like users, posts, products)
    
*   Each resource has a URL (e.g., `/users`, `/posts`, `/products`)
    
*   You interact with resources using standard HTTP methods (GET, POST, PUT, DELETE)
    
*   The server responds with standard HTTP status codes (200, 404, 500, etc.)
    

REST APIs are stateless, meaning each request contains all the information the server needs to process it. The server does not remember previous requests. Every request is independent.

The reason REST became the standard is that it maps cleanly to HTTP, which was already built for this. HTTP has methods (GET, POST, PUT, DELETE). HTTP has status codes (200, 404, 500). REST just says: use these in a consistent way.

* * *

## Resources Are the Core Concept

In REST, everything is a resource. A resource is any data your API exposes: users, blog posts, products, orders, comments, anything.

Each resource gets a base URL:

```cpp
/users       → represents all users
/posts       → represents all posts
/products    → represents all products
```

An ID identifies a specific resource:

```javascript
/users/123       → represents the user with ID 123
/posts/456       → represents the post with ID 456
```

Notice the pattern: plural noun for collections, singular ID for specific items. This is a REST convention. You could use `/user/123` instead of `/users/123`, but most REST APIs use plural.

* * *

## HTTP Methods Map to Actions

REST uses HTTP methods to define what action you want to perform on a resource:

*   **GET** - Retrieve data (does not modify anything)
    
*   **POST** - Create new data
    
*   **PUT** - Update existing data (replace entirely)
    
*   **PATCH** - Update existing data (partial modification)
    
*   **DELETE** - Remove data
    

These map to the standard CRUD operations (Create, Read, Update, Delete):

| CRUD Operation | HTTP Method |
| --- | --- |
| Create | POST |
| Read | GET |
| Update | PUT/PATCH |
| Delete | DELETE |

The beauty of REST is that the combination of URL and method tells you everything about what the request does:

```javascript
GET /users           → Get all users
GET /users/123       → Get user 123
POST /users          → Create a new user
PUT /users/123       → Update user 123 (full replacement)
DELETE /users/123    → Delete user 123
```

You do not need route names like `/getUserById` or `/deleteUser`. The method and URL are enough.

![](https://cdn.hashnode.com/uploads/covers/69517a64b415eddac99049ec/5c90258f-11c8-49d5-a9a7-be1819cfabeb.png align="center")

* * *

## Building a Users API in Express

Let's build a real REST API for managing users. We will start simple and add features as we go.

### Basic Setup

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

app.use(express.json()); // parse JSON request bodies

// In-memory data store (normally this would be a database)
let users = [
  { id: 1, name: "Ishan", email: "ishan@example.com", role: "SDE" },
  { id: 2, name: "Arjun", email: "arjun@example.com", role: "Developer" },
  { id: 3, name: "Priya", email: "priya@example.com", role: "Designer" }
];

let nextId = 4; // for generating new user IDs

app.listen(3000, () => {
  console.log("API running on http://localhost:3000");
});
```

We are using an in-memory array for simplicity. In production, this would be a database.

* * *

## GET - Retrieving Resources

### Get All Users

```js
app.get("/users", (req, res) => {
  res.json(users);
});
```

Request:

```plaintext
GET http://localhost:3000/users
```

Response:

```json
[
  { "id": 1, "name": "Ishan", "email": "ishan@example.com", "role": "SDE" },
  { "id": 2, "name": "Arjun", "email": "arjun@example.com", "role": "Developer" },
  { "id": 3, "name": "Priya", "email": "priya@example.com", "role": "Designer" }
]
```

The route returns all users as a JSON array. Simple and predictable.

### Get a Specific User

```js
app.get("/users/:id", (req, res) => {
  const userId = parseInt(req.params.id);
  const user   = users.find(u => u.id === userId);

  if (!user) {
    return res.status(404).json({ error: "User not found" });
  }

  res.json(user);
});
```

Request:

```plaintext
GET http://localhost:3000/users/1
```

Response:

```json
{ "id": 1, "name": "Ishan", "email": "ishan@example.com", "role": "SDE" }
```

If the user does not exist:

Request:

```plaintext
GET http://localhost:3000/users/999
```

Response (404):

```json
{ "error": "User not found" }
```

Notice the `404` status code. This is important. Status codes communicate what happened without the client needing to parse the response body.

* * *

## POST - Creating Resources

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

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

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

  users.push(newUser);

  res.status(201).json(newUser);
});
```

Request:

```plaintext
POST http://localhost:3000/users
Content-Type: application/json

{
  "name": "Ravi",
  "email": "ravi@example.com",
  "role": "QA Engineer"
}
```

Response (201):

```json
{
  "id": 4,
  "name": "Ravi",
  "email": "ravi@example.com",
  "role": "QA Engineer"
}
```

The `201 Created` status code indicates a new resource was successfully created. The response body contains the created resource, including the server-generated ID.

If validation fails:

Request:

```plaintext
POST http://localhost:3000/users
Content-Type: application/json

{
  "name": "Ravi"
}
```

Response (400):

```json
{ "error": "Name, email, and role are required" }
```

`400 Bad Request` means the client sent invalid data.

![](https://cdn.hashnode.com/uploads/covers/69517a64b415eddac99049ec/e3a95cee-75c9-4e99-ad8d-4e153c4a9aed.png align="center")

* * *

## PUT - Updating Resources

PUT replaces the entire resource with the provided data.

```js
app.put("/users/:id", (req, res) => {
  const userId = parseInt(req.params.id);
  const { name, email, role } = req.body;

  const userIndex = users.findIndex(u => u.id === userId);

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

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

  users[userIndex] = {
    id: userId,
    name: name,
    email: email,
    role: role
  };

  res.json(users[userIndex]);
});
```

Request:

```plaintext
PUT http://localhost:3000/users/1
Content-Type: application/json

{
  "name": "Ishan Parnami",
  "email": "ishan.new@example.com",
  "role": "Senior SDE"
}
```

Response (200):

```json
{
  "id": 1,
  "name": "Ishan Parnami",
  "email": "ishan.new@example.com",
  "role": "Senior SDE"
}
```

The entire user object was replaced. If you only send `name` and omit `email` and `role`, the validation rejects it because PUT expects the full resource.

* * *

## PATCH - Partial Updates

PATCH updates only the fields you provide. It is more flexible than PUT.

```js
app.patch("/users/:id", (req, res) => {
  const userId = parseInt(req.params.id);
  const updates = req.body;

  const userIndex = users.findIndex(u => u.id === userId);

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

  // Apply only the provided fields
  users[userIndex] = {
    ...users[userIndex],
    ...updates,
    id: userId // ensure ID cannot be changed
  };

  res.json(users[userIndex]);
});
```

Request:

```plaintext
PATCH http://localhost:3000/users/1
Content-Type: application/json

{
  "role": "Lead SDE"
}
```

Response (200):

```json
{
  "id": 1,
  "name": "Ishan Parnami",
  "email": "ishan.new@example.com",
  "role": "Lead SDE"
}
```

Only the `role` field changed. `name` and `email` remained the same. PATCH is useful for updating individual fields without sending the entire object.

* * *

## DELETE - Removing Resources

```js
app.delete("/users/:id", (req, res) => {
  const userId = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === userId);

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

  const deletedUser = users[userIndex];
  users.splice(userIndex, 1);

  res.json({ message: "User deleted", user: deletedUser });
});
```

Request:

```plaintext
DELETE http://localhost:3000/users/2
```

Response (200):

```json
{
  "message": "User deleted",
  "user": { "id": 2, "name": "Arjun", "email": "arjun@example.com", "role": "Developer" }
}
```

Some APIs return `204 No Content` for DELETE requests instead of `200 OK` with a response body. Both are valid. `204` means "success, but no content to return." `200` means "success, here is confirmation of what was deleted."

* * *

## HTTP Status Codes Explained Simply

Status codes communicate what happened without the client needing to read the response body. They are grouped by category:

### 2xx - Success

*   **200 OK** - Request succeeded. Used for GET, PUT, PATCH, DELETE.
    
*   **201 Created** - Resource was created. Used for POST.
    
*   **204 No Content** - Success, but no response body. Sometimes used for DELETE.
    

### 4xx - Client Errors

*   **400 Bad Request** - Invalid data sent by the client (validation failure).
    
*   **401 Unauthorized** - Authentication required but not provided.
    
*   **403 Forbidden** - Authenticated, but not authorized to access this resource.
    
*   **404 Not Found** - Resource does not exist.
    

### 5xx - Server Errors

*   **500 Internal Server Error** - Something went wrong on the server.
    
*   **503 Service Unavailable** - Server is temporarily down.
    

In our users API:

*   `200` - GET, PUT, PATCH, DELETE succeeded
    
*   `201` - POST created a new user
    
*   `400` - Invalid request body (missing required fields)
    
*   `404` - User ID does not exist
    
*   `500` - Something crashed on the server (caught by error handler)
    

* * *

## Route Organization Best Practices

REST APIs follow predictable URL patterns. Here are the conventions:

### Use Plural Nouns

```plaintext
Good:  /users, /posts, /products
Bad:   /user, /post, /product
```

Plural is standard because `/users` represents a collection of users.

### Use Nested Routes for Relationships

If posts belong to users:

```plaintext
GET /users/1/posts        → Get all posts by user 1
GET /users/1/posts/5      → Get post 5 by user 1
POST /users/1/posts       → Create a new post for user 1
```

The relationship is clear from the URL structure.

### Avoid Verbs in URLs

```plaintext
Good:  GET /users/1
Bad:   GET /getUser/1

Good:  POST /users
Bad:   POST /createUser

Good:  DELETE /users/1
Bad:   DELETE /deleteUser/1
```

The HTTP method is the verb. The URL is the noun (the resource).

### Use Query Parameters for Filtering, Sorting, Pagination

```plaintext
GET /users?role=SDE               → Filter by role
GET /users?sort=name              → Sort by name
GET /users?page=2&limit=10        → Pagination
```

Query parameters modify the request but do not change the resource being accessed.

Example implementation:

```js
app.get("/users", (req, res) => {
  let result = users;

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

  // Sort by name
  if (req.query.sort === "name") {
    result = result.sort((a, b) => a.name.localeCompare(b.name));
  }

  res.json(result);
});
```

Request:

```plaintext
GET http://localhost:3000/users?role=SDE&sort=name
```

Response:

```json
[
  { "id": 1, "name": "Ishan", "email": "ishan@example.com", "role": "SDE" }
]
```

* * *

## Complete Users API Example

Here is the full API with all CRUD operations, validation, and error handling:

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

app.use(express.json());

let users = [
  { id: 1, name: "Ishan", email: "ishan@example.com", role: "SDE" },
  { id: 2, name: "Arjun", email: "arjun@example.com", role: "Developer" },
  { id: 3, name: "Priya", email: "priya@example.com", role: "Designer" }
];

let nextId = 4;

// GET all users
app.get("/users", (req, res) => {
  let result = users;

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

  if (req.query.sort === "name") {
    result = result.sort((a, b) => a.name.localeCompare(b.name));
  }

  res.json(result);
});

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

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

  const newUser = { id: nextId++, name, email, role };
  users.push(newUser);

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

// PUT update user (full replacement)
app.put("/users/:id", (req, res) => {
  const userId = parseInt(req.params.id);
  const { name, email, role } = req.body;

  const userIndex = users.findIndex(u => u.id === userId);

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

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

  users[userIndex] = { id: userId, name, email, role };
  res.json(users[userIndex]);
});

// PATCH update user (partial)
app.patch("/users/:id", (req, res) => {
  const userId = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === userId);

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

  users[userIndex] = { ...users[userIndex], ...req.body, id: userId };
  res.json(users[userIndex]);
});

// DELETE user
app.delete("/users/:id", (req, res) => {
  const userId = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === userId);

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

  const deletedUser = users[userIndex];
  users.splice(userIndex, 1);

  res.json({ message: "User deleted", user: deletedUser });
});

// Error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: "Something went wrong" });
});

app.listen(3000, () => {
  console.log("API running on http://localhost:3000");
});
```

This is a fully functional REST API. You can test it with tools like Postman, Insomnia, or `curl`.

* * *

## Testing the API with curl

```bash
# Get all users
curl http://localhost:3000/users

# Get user 1
curl http://localhost:3000/users/1

# Create a new user
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Ravi","email":"ravi@example.com","role":"QA"}'

# Update user 1
curl -X PUT http://localhost:3000/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Ishan Parnami","email":"ishan@example.com","role":"Senior SDE"}'

# Partially update user 1
curl -X PATCH http://localhost:3000/users/1 \
  -H "Content-Type: application/json" \
  -d '{"role":"Lead SDE"}'

# Delete user 2
curl -X DELETE http://localhost:3000/users/2

# Filter users by role
curl http://localhost:3000/users?role=SDE
```

* * *

## Why REST Matters

Before REST became standard, every API was different. One API might use `/getUserById?id=123`, another might use `/user/get/123`, another might use `/api/v1/users/fetch/123`. Clients had to learn each API individually.

REST standardized the patterns:

*   Resources are nouns (`/users`)
    
*   Actions are HTTP methods (`GET`, `POST`, etc.)
    
*   Responses use standard status codes (`200`, `404`, etc.)
    

This makes APIs predictable. If you know REST, you can guess how an API works without reading documentation. You know that `GET /users/123` retrieves a user and `DELETE /users/123` deletes a user.

REST is not perfect. It has limitations (deeply nested resources can get awkward, and some operations do not map cleanly to CRUD). But for most CRUD-based APIs, REST is simple, consistent, and works well.

* * *

## Quick Recap

REST is an architectural style for building HTTP-based APIs where resources are exposed at URLs and manipulated using standard HTTP methods. Resources are nouns like `/users` or `/posts`. HTTP methods are verbs: GET retrieves, POST creates, PUT updates (full replacement), PATCH updates (partial), DELETE removes. Status codes communicate success (2xx), client errors (4xx), or server errors (5xx) without parsing the response body. RESTful URLs are predictable: `/users` for all users, `/users/:id` for a specific user. Avoid verbs in URLs and use query parameters for filtering, sorting, and pagination. The combination of method, URL, and status code makes REST APIs intuitive and self-documenting.
