REST API Basics

REST (Representational State Transfer) is an architectural style for designing networked applications. It relies on a stateless, client-server, cacheable communications protocol — and in virtually all cases, the HTTP protocol is used.

REST is not a protocol or a standard but a set of architectural constraints. API developers can implement REST in a variety of ways.

Core Principles

For an API to be considered RESTful, it typically conforms to the following constraints:

The client (frontend) and server (backend) act independently. The client should know how to fetch data, and the server should know how to serve it. This separation allows for portability and scalability.

The server does not store any state about the client session on the server side. Each request from the client to server must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server.

Clients can cache responses. Responses must define themselves as cacheable or not to prevent clients from reusing stale or inappropriate data in response to further requests.

URLs should represent resources, not actions. Use nouns instead of verbs.

  • Bad: ,
  • Good: ,

Always use HTTP methods semantically to improve clarity and consistency. Each HTTP method has a clear meaning:

| Code | Meaning           |
|------|-------------------|
| 200  | OK                |
| 201  | Created           |
| 204  | No Content        |
| 400  | Bad Request       |
| 401  | Unauthorized      |
| 403  | Forbidden         |
| 404  | Not Found         |
| 409  | Conflict          |
| 429  | Too Many Requests |
| 500  | Server Error      |

Responses should follow a consistent format. Consistency makes APIs easier to consume and debug.

Example:

{
  "success": true,
  "message": "Fetched successfully",
  "data": {...},
  "statusCode": 200
}

Always version your API to handle breaking changes gracefully.

/api/v1/users
/api/v2/users

Servers can temporarily extend or customize the functionality of a client by the transfer of executable code (e.g., scripts).

Prevent large data dumps by implementing query parameters.

Express Implementation Example:

app.get("/api/users", (req, res) => {
  const { page = 1, limit = 10, role } = req.query;
 
  const options = {
    page: parseInt(page),
    limit: parseInt(limit),
    filter: role ? { role } : {}
  };
 
  const result = db.users.find(options);
  res.json(result);
});

HTTP Methods

In a REST API, endpoints (URLs) represent resources, and HTTP verbs (methods) represent the actions performed on those resources.

Here is how you implement them in Node.js with Express:

Retrieve a representation of a resource. This should not modify the server state.

// GET /api/users - Get all users
app.get("/api/users", (req, res) => {
  // Assume db is your database instance
  const users = db.users.findAll();
  res.status(200).json(users);
});
 
// GET /api/users/:id - Get a specific user
app.get("/api/users/:id", (req, res) => {
  const { id } = req.params;
  const user = db.users.findById(id);
 
  if (!user) {
    return res.status(404).json({ error: "User not found" });
  }
 
  res.status(200).json(user);
});

Create a new resource. The payload is sent in the request body.

// POST /api/users - Create a new user
app.post("/api/users", (req, res) => {
  const { name, email } = req.body;
 
  if (!name || !email) {
    return res.status(400).json({ error: "Name and email are required" });
  }
 
  const newUser = db.users.create({ name, email });
 
  // 201 Created is the standard status code for creation
  res.status(201).json(newUser);
});

Update or Replace a resource entirely. If the resource doesn't exist, it can be created (depending on implementation).

// PUT /api/users/:id - Replace user data
app.put("/api/users/:id", (req, res) => {
  const { id } = req.params;
  const newData = req.body;
 
  // This typically replaces the entire object
  const updatedUser = db.users.update(id, newData);
 
  res.status(200).json(updatedUser);
});

Partially Update a resource. Only the fields sent in the body are updated.

// PATCH /api/users/:id - Update specific fields
app.patch("/api/users/:id", (req, res) => {
  const { id } = req.params;
  const updates = req.body;
 
  const updatedUser = db.users.patch(id, updates);
 
  res.status(200).json(updatedUser);
});

Remove a resource.

// DELETE /api/users/:id - Delete a user
app.delete("/api/users/:id", (req, res) => {
  const { id } = req.params;
 
  db.users.delete(id);
 
  // 204 No Content is often used when there is no response body meant to be sent
  res.status(204).send();
});

HTTP Status Codes

Using the correct status code is crucial for a REST API to communicate clearly with the client.

| Code | Meaning           |
|------|-------------------|
| 200  | OK                |
| 201  | Created           |
| 204  | No Content        |
| 400  | Bad Request       |
| 401  | Unauthorized      |
| 403  | Forbidden         |
| 404  | Not Found         |
| 409  | Conflict          |
| 429  | Too Many Requests |
| 500  | Server Error      |
export const STATUS_CODES = {
  // 2xx Success
  OK: 200,
  CREATED: 201,
  ACCEPTED: 202,
  NO_CONTENT: 204,
 
  // 3xx Redirection
  MOVED_PERMANENTLY: 301,
  FOUND: 302,
  NOT_MODIFIED: 304,
 
  // 4xx Client Errors
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
  CONFLICT: 409,
  UNPROCESSABLE_ENTITY: 422,
  TOO_MANY_REQUESTS: 429,
 
  // 5xx Server Errors
  INTERNAL_SERVER_ERROR: 500,
  NOT_IMPLEMENTED: 501,
  BAD_GATEWAY: 502,
  SERVICE_UNAVAILABLE: 503,
  GATEWAY_TIMEOUT: 504
} as const;

For more info visit HTTP Status Codes Docs


Security Best Practices

Securing a REST API is critical for production environments. Below are practical measures you can implement in Node.js.

Always serve your API over HTTPS. SSL/TLS encrypts data in transit, preventing man-in-the-middle attacks.

Use Helmet to set various HTTP headers that help secure your app.

npm install helmet
import helmet from "helmet";
 
app.use(helmet());

Prevent brute-force attacks and abuse by limiting the number of requests from the same IP.

npm install express-rate-limit
import rateLimit from "express-rate-limit";
 
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: "Too many requests from this IP, please try again later."
});
 
// Apply to all requests
app.use(limiter);

Cross-Origin Resource Sharing (CORS) controls which domains can access your API.

npm install cors
import cors from "cors";
 
const corsOptions = {
  origin: "https://your-frontend-domain.com", // Restrict to trusted domains
  methods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"]
};
 
app.use(cors(corsOptions));

Never trust client input. Validate request bodies, params, and query strings. Zod is a great library for this.

npm install zod
import { z } from "zod";
 
const userSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
});
 
app.post("/api/users", (req, res) => {
  const result = userSchema.safeParse(req.body);
 
  if (!result.success) {
    return res.status(400).json({
      error: "Invalid input",
      details: result.error.issues
    });
  }
 
  // Proceed with creating user...
});

Ensure only authenticated users can access protected resources.

// Simple middleware example using JWT
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1]; // Bearer TOKEN
 
  if (!token) return res.sendStatus(401);
 
  jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
};
 
app.get("/api/protected", authenticateToken, (req, res) => {
  // ...
});

Do not leak stack traces or sensitive database errors to the client in production.

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack); // Log internally
 
  const statusCode = err.statusCode || 500;
  const message =
    process.env.NODE_ENV === "production"
      ? "Internal Server Error"
      : err.message;
 
  res.status(statusCode).json({
    success: false,
    message
  });
});