Skip to main content

Command Palette

Search for a command to run...

REST API Design Made Simple with Express.js

Published
8 min read
REST API Design Made Simple with Express.js

APIs are how applications communicate. A REST API is a structured way to request and receive data over HTTP. Building REST APIs is one of the most common uses for Express.

What is an API?

An API (Application Programming Interface) is a contract between client and server. It says: "Send me this format, and I'll respond with that format."

For example, a social media API might say: "POST to /posts with a message, and I'll create a post."

What is REST?

REST (Representational State Transfer) is a style of designing APIs. It uses HTTP methods (GET, POST, PUT, DELETE) and URLs to represent operations on resources.

A resource is something you can create, read, update, or delete. Examples: users, posts, comments, products.

Instead of having endpoints like:

  • /createUser
  • /getUserData
  • /deleteUser

REST uses the resource URL with HTTP methods:

  • GET /users - Get all users
  • POST /users - Create a user
  • GET /users/123 - Get user 123
  • PUT /users/123 - Update user 123
  • DELETE /users/123 - Delete user 123

This is cleaner and more logical.

HTTP Methods for CRUD

REST maps HTTP methods to database operations:

HTTP Method Operation Meaning
GET Read Retrieve data
POST Create Add new data
PUT Update Modify existing data
DELETE Delete Remove data

These four methods handle all basic operations (CRUD: Create, Read, Update, Delete).

Basic REST API Example

Here's a simple REST API for managing blog posts:

import express from 'express';

const app = express();
app.use(express.json());

// Sample data
let posts = [
  { id: 1, title: 'First Post', content: 'Hello world' },
  { id: 2, title: 'Second Post', content: 'Another post' }
];

// GET all posts
app.get('/posts', (req, res) => {
  res.json(posts);
});

// GET a single post
app.get('/posts/:id', (req, res) => {
  const post = posts.find(p => p.id === parseInt(req.params.id));
  if (!post) {
    return res.status(404).json({ error: 'Post not found' });
  }
  res.json(post);
});

// CREATE a new post
app.post('/posts', (req, res) => {
  const newPost = {
    id: posts.length + 1,
    title: req.body.title,
    content: req.body.content
  };
  posts.push(newPost);
  res.status(201).json(newPost);
});

// UPDATE a post
app.put('/posts/:id', (req, res) => {
  const post = posts.find(p => p.id === parseInt(req.params.id));
  if (!post) {
    return res.status(404).json({ error: 'Post not found' });
  }
  post.title = req.body.title || post.title;
  post.content = req.body.content || post.content;
  res.json(post);
});

// DELETE a post
app.delete('/posts/:id', (req, res) => {
  const index = posts.findIndex(p => p.id === parseInt(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: 'Post not found' });
  }
  const deleted = posts.splice(index, 1);
  res.json({ message: 'Post deleted', post: deleted[0] });
});

app.listen(3000);

This API covers all CRUD operations for posts.

Naming Conventions

Follow these conventions for clean URLs:

  • Use plural nouns for resources: /posts, /users, /products (not /post, /user)
  • Use lowercase: /posts (not /Posts)
  • Use hyphens for multi-word resources: /blog-posts (not /blogposts or /blog_posts)
  • Avoid verbs in URLs: Use HTTP methods instead
    • Wrong: GET /posts/getAll
    • Right: GET /posts

Status Codes

Use appropriate HTTP status codes:

Code Meaning Use When
200 OK Request succeeded
201 Created Resource created successfully
400 Bad Request Client error (invalid data)
401 Unauthorized Authentication required
403 Forbidden Authenticated but not allowed
404 Not Found Resource doesn't exist
500 Server Error Server-side error
app.get('/posts/:id', (req, res) => {
  const post = posts.find(p => p.id === parseInt(req.params.id));
  if (!post) {
    return res.status(404).json({ error: 'Post not found' });
  }
  res.status(200).json(post); // 200 is default, can omit
});

app.post('/posts', (req, res) => {
  const newPost = { id: posts.length + 1, ...req.body };
  posts.push(newPost);
  res.status(201).json(newPost);
});

Handling Query Parameters

Use query parameters for filtering and sorting:

app.get('/posts', (req, res) => {
  let results = posts;

  // Filter by status
  if (req.query.status) {
    results = results.filter(p => p.status === req.query.status);
  }

  // Sort by date
  if (req.query.sort === 'date') {
    results.sort((a, b) => new Date(b.date) - new Date(a.date));
  }

  // Pagination
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const start = (page - 1) * limit;
  results = results.slice(start, start + limit);

  res.json(results);
});

Usage:

  • GET /posts?status=published
  • GET /posts?sort=date
  • GET /posts?page=2&limit=5

Error Handling

Consistent error responses help clients:

app.get('/posts/:id', (req, res) => {
  const post = posts.find(p => p.id === parseInt(req.params.id));
  if (!post) {
    return res.status(404).json({
      error: {
        code: 'POST_NOT_FOUND',
        message: 'The requested post does not exist',
        details: `Post with ID ${req.params.id} not found`
      }
    });
  }
  res.json(post);
});

Structured error responses let clients handle errors properly.

Request Validation

Validate data before processing:

app.post('/posts', (req, res) => {
  const { title, content } = req.body;

  // Validation
  if (!title || !content) {
    return res.status(400).json({
      error: 'Title and content are required'
    });
  }

  if (title.length < 3) {
    return res.status(400).json({
      error: 'Title must be at least 3 characters'
    });
  }

  const newPost = { id: posts.length + 1, title, content };
  posts.push(newPost);
  res.status(201).json(newPost);
});

API Versioning

As your API grows, you might need to change it. Versioning helps:

app.get('/v1/posts', (req, res) => {
  // Version 1 implementation
  res.json(posts);
});

app.get('/v2/posts', (req, res) => {
  // Version 2 implementation (different format, more data, etc.)
  res.json(posts.map(p => ({
    id: p.id,
    title: p.title,
    description: p.content,
    createdAt: new Date()
  })));
});

This lets old clients continue using v1 while new clients use v2.

Authentication in REST APIs

Protect routes with authentication:

function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  // Verify token (simplified)
  req.user = { id: 1 };
  next();
}

app.post('/posts', authenticate, (req, res) => {
  // Create post as authenticated user
  const newPost = {
    id: posts.length + 1,
    ...req.body,
    userId: req.user.id
  };
  posts.push(newPost);
  res.status(201).json(newPost);
});

app.delete('/posts/:id', authenticate, (req, res) => {
  // Delete logic
});

Only authenticated users can create or delete.

CORS for Cross-Origin Requests

If your API is called from a different domain, enable CORS:

npm install cors
import cors from 'cors';

app.use(cors());
// or restrict to specific domains
app.use(cors({
  origin: 'https://example.com'
}));

Complete REST API Example

Here's a realistic REST API with authentication, validation, and proper status codes:

import express from 'express';

const app = express();
app.use(express.json());

let posts = [];
let nextId = 1;

// Middleware: log requests
app.use((req, res, next) => {
  console.log(`\({req.method} \){req.path}`);
  next();
});

// Middleware: authenticate
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (token === 'valid-token') {
    req.user = { id: 1 };
    next();
  } else {
    res.status(401).json({ error: 'Unauthorized' });
  }
}

// GET all posts
app.get('/posts', (req, res) => {
  res.json(posts);
});

// GET single post
app.get('/posts/:id', (req, res) => {
  const post = posts.find(p => p.id === parseInt(req.params.id));
  if (!post) return res.status(404).json({ error: 'Not found' });
  res.json(post);
});

// POST create post (requires auth)
app.post('/posts', authenticate, (req, res) => {
  const { title, content } = req.body;

  if (!title || !content) {
    return res.status(400).json({ error: 'Title and content required' });
  }

  const newPost = { id: nextId++, title, content, userId: req.user.id };
  posts.push(newPost);
  res.status(201).json(newPost);
});

// PUT update post
app.put('/posts/:id', authenticate, (req, res) => {
  const post = posts.find(p => p.id === parseInt(req.params.id));
  if (!post) return res.status(404).json({ error: 'Not found' });

  post.title = req.body.title || post.title;
  post.content = req.body.content || post.content;
  res.json(post);
});

// DELETE post
app.delete('/posts/:id', authenticate, (req, res) => {
  const index = posts.findIndex(p => p.id === parseInt(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'Not found' });

  posts.splice(index, 1);
  res.json({ message: 'Deleted' });
});

app.listen(3000);

Key Takeaways

  • REST uses HTTP methods (GET, POST, PUT, DELETE) to represent operations
  • Resources are nouns, not verbs: /posts not /getPosts
  • Status codes indicate operation success: 200, 201, 400, 404, 500
  • Query parameters filter and sort results
  • Validation catches bad data before processing
  • Authentication protects sensitive operations
  • Consistent error responses help clients handle errors
  • Versioning lets you evolve your API without breaking clients

Build REST APIs following these principles, and you'll create clean, intuitive, maintainable APIs that developers love to use.