Beginner
25 min
Full Guide

REST APIs

Learn RESTful API design principles, HTTP methods, endpoints, and status codes

What is REST?

REST (Representational State Transfer) is an architectural style for designing networked applications. Created by Roy Fielding in 2000, REST has become the dominant web API design pattern due to its simplicity, scalability, and use of standard HTTP protocols.

A RESTful API is one that adheres to REST principles, providing a uniform interface for interacting with resources.

REST Principles

🔗 Uniform Interface

Consistent way to interact with resources using standard HTTP methods and URLs.

🚫 Stateless

Each request contains all information needed. Server doesn't store client state.

📦 Cacheable

Responses can be cached to improve performance and reduce server load.

📊 Layered System

Client can't tell if connected directly to server or through intermediary.

🖥️ Client-Server

Separation of concerns between client (UI) and server (data).

📝 Resource-Based

Everything is a resource identified by URIs. Resources have representations (JSON/XML).

RESTful URL Structure

REST APIs use descriptive, hierarchical URLs to represent resources:

// Resource URL Structure
https://api.example.com/v1/users              // Collection of users
https://api.example.com/v1/users/123          // Single user (ID: 123)
https://api.example.com/v1/users/123/posts    // User's posts
https://api.example.com/v1/users/123/posts/456 // Specific post

// URL Components:
// Protocol: https://
// Host: api.example.com
// Version: /v1
// Resource: /users
// Identifier: /123
// Sub-resource: /posts

✅ Good URL Practices:

  • • Use nouns, not verbs: /users not /getUsers
  • • Use plural nouns: /users not /user
  • • Use lowercase: /users not /Users
  • • Use hyphens for multi-word: /user-profiles not /userProfiles

HTTP Methods in Detail

GET Retrieve Resource(s)
// GET - Retrieve all users
const response = await fetch('https://api.example.com/users');
const users = await response.json();

// GET - Retrieve single user
const user = await fetch('https://api.example.com/users/123');
const userData = await user.json();

// GET with query parameters (filtering/pagination)
const filteredUsers = await fetch(
  'https://api.example.com/users?role=admin&page=1&limit=10'
);

// Response: 200 OK
{
  "data": [
    { "id": 1, "name": "John Doe", "role": "admin" }
  ],
  "pagination": { "page": 1, "limit": 10, "total": 45 }
}
POST Create New Resource
// POST - Create new user
const newUser = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({
    name: 'Jane Smith',
    email: 'jane@example.com',
    role: 'user'
  })
});

const created = await newUser.json();

// Response: 201 Created
{
  "id": 124,
  "name": "Jane Smith",
  "email": "jane@example.com",
  "role": "user",
  "createdAt": "2024-01-15T10:30:00Z"
}
PUT Replace Entire Resource
// PUT - Replace entire user (all fields required)
const updatedUser = await fetch('https://api.example.com/users/124', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({
    name: 'Jane Doe',        // Changed
    email: 'jane@example.com', // Same
    role: 'admin'            // Changed
  })
});

// Response: 200 OK
{
  "id": 124,
  "name": "Jane Doe",
  "email": "jane@example.com",
  "role": "admin",
  "updatedAt": "2024-01-15T11:00:00Z"
}
PATCH Partial Update
// PATCH - Update only specific fields
const patchedUser = await fetch('https://api.example.com/users/124', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({
    role: 'moderator'  // Only updating role
  })
});

// Response: 200 OK
{
  "id": 124,
  "name": "Jane Doe",
  "email": "jane@example.com",
  "role": "moderator",
  "updatedAt": "2024-01-15T11:30:00Z"
}
DELETE Remove Resource
// DELETE - Remove user
const deleteResponse = await fetch('https://api.example.com/users/124', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer token123'
  }
});

// Response: 204 No Content (empty body)
// OR
// Response: 200 OK
{
  "message": "User successfully deleted",
  "deletedId": 124
}

HTTP Status Codes

Status codes indicate the result of an HTTP request:

✅ 2xx Success

200 OK - Request succeeded
201 Created - Resource created
204 No Content - Success, no body
202 Accepted - Processing queued

↪️ 3xx Redirection

301 Moved Permanently - New URL
302 Found - Temporary redirect
304 Not Modified - Use cache
307 Temporary Redirect

⚠️ 4xx Client Errors

400 Bad Request - Invalid syntax
401 Unauthorized - Auth required
403 Forbidden - No permission
404 Not Found - Resource missing
422 Unprocessable - Validation error
429 Too Many Requests - Rate limited

❌ 5xx Server Errors

500 Internal Error - Server error
502 Bad Gateway - Invalid upstream
503 Unavailable - Server down
504 Gateway Timeout - Upstream timeout

Complete REST API Client

Building a reusable API client class:

// REST API Client Class
class ApiClient {
  constructor(baseUrl, token = null) {
    this.baseUrl = baseUrl;
    this.token = token;
  }

  setToken(token) {
    this.token = token;
  }

  getHeaders() {
    const headers = {
      'Content-Type': 'application/json',
    };
    if (this.token) {
      headers['Authorization'] = `Bearer ${this.token}`;
    }
    return headers;
  }

  async request(method, endpoint, data = null) {
    const url = `${this.baseUrl}${endpoint}`;
    const options = {
      method,
      headers: this.getHeaders(),
    };

    if (data && ['POST', 'PUT', 'PATCH'].includes(method)) {
      options.body = JSON.stringify(data);
    }

    try {
      const response = await fetch(url, options);
      const contentType = response.headers.get('content-type');
      let responseData;
      
      if (contentType?.includes('application/json')) {
        responseData = await response.json();
      } else {
        responseData = await response.text();
      }

      if (!response.ok) {
        throw {
          status: response.status,
          message: responseData.message || 'Request failed',
          data: responseData
        };
      }

      return responseData;
    } catch (error) {
      console.error(`API Error [${method}] ${endpoint}:`, error);
      throw error;
    }
  }

  // Convenience methods
  get(endpoint) { return this.request('GET', endpoint); }
  post(endpoint, data) { return this.request('POST', endpoint, data); }
  put(endpoint, data) { return this.request('PUT', endpoint, data); }
  patch(endpoint, data) { return this.request('PATCH', endpoint, data); }
  delete(endpoint) { return this.request('DELETE', endpoint); }
}

// Usage Example
const api = new ApiClient('https://api.example.com/v1');
api.setToken('your-jwt-token');

// CRUD Operations
async function userOperations() {
  // Create
  const newUser = await api.post('/users', {
    name: 'John Doe',
    email: 'john@example.com'
  });

  // Read
  const users = await api.get('/users');
  const user = await api.get(`/users/${newUser.id}`);

  // Update
  const updated = await api.patch(`/users/${newUser.id}`, {
    name: 'John Smith'
  });

  // Delete
  await api.delete(`/users/${newUser.id}`);
}

userOperations();

Query Parameters

Use query parameters for filtering, sorting, and pagination:

// Building URLs with Query Parameters
class QueryBuilder {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.params = new URLSearchParams();
  }

  page(page, limit = 10) {
    this.params.set('page', page);
    this.params.set('limit', limit);
    return this;
  }

  filter(field, value) {
    this.params.set(field, value);
    return this;
  }

  sort(field, order = 'asc') {
    this.params.set('sort', field);
    this.params.set('order', order);
    return this;
  }

  search(query) {
    this.params.set('q', query);
    return this;
  }

  fields(fieldList) {
    this.params.set('fields', fieldList.join(','));
    return this;
  }

  build() {
    const queryString = this.params.toString();
    return queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl;
  }
}

// Usage
const url = new QueryBuilder('https://api.example.com/users')
  .page(2, 20)
  .filter('role', 'admin')
  .sort('createdAt', 'desc')
  .search('john')
  .fields(['id', 'name', 'email'])
  .build();

console.log(url);
// https://api.example.com/users?page=2&limit=20&role=admin&sort=createdAt&order=desc&q=john&fields=id,name,email

Response Format Standards

// Consistent API Response Structure

// Success Response
{
  "success": true,
  "data": {
    "id": 123,
    "name": "John Doe",
    "email": "john@example.com"
  },
  "meta": {
    "timestamp": "2024-01-15T10:30:00Z"
  }
}

// Collection Response with Pagination
{
  "success": true,
  "data": [
    { "id": 1, "name": "John" },
    { "id": 2, "name": "Jane" }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 100,
    "totalPages": 10,
    "hasNext": true,
    "hasPrev": false
  }
}

// Error Response
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input data",
    "details": [
      { "field": "email", "message": "Invalid email format" },
      { "field": "age", "message": "Must be a positive number" }
    ]
  }
}

💡 REST API Best Practices

  • Use proper HTTP methods - GET for reading, POST for creating, etc.
  • Return appropriate status codes - 201 for created, 404 for not found
  • Version your API - Use /v1/, /v2/ in URLs
  • Use JSON for data - Standard format for web APIs
  • Implement pagination - Don't return unlimited results
  • Document your API - Use OpenAPI/Swagger