Best Practices for REST APIs

Engineering

APIs

Best Practices

Summary

This article provides a comprehensive guide to REST API best practices, covering areas such as consistent naming conventions, proper use of HTTP methods, statelessness, versioning, and backward compatibility. Security practices like using DTOs, authentication, and input validation are emphasized. Performance optimizations include filtering, sorting, pagination, and caching, while monitoring and logging are highlighted for maintaining API health.

Key insights:
  • Consistent Naming Conventions: Use nouns for resources, plural for collections, and follow hierarchical structures in URIs to ensure clarity and predictability.

  • Correct Use of HTTP Methods: Apply GET, POST, PUT, DELETE appropriately based on their idempotency, safety, and cacheability to maintain RESTful principles.

  • Error Handling with Standard HTTP Codes: Use appropriate status codes (e.g., 200 OK, 400 Bad Request) to clearly communicate the outcome of requests to clients.

  • Statelessness: Ensure each request contains all necessary information without relying on server-stored session data, improving scalability.

  • Security Best Practices: Use HTTPS for encryption, JWT for authentication, input validation to prevent attacks, and DTOs to protect sensitive internal models from exposure.

  • Versioning for Backward Compatibility: Implement versioning early to avoid breaking changes and ensure backward compatibility with older clients.

  • Caching: Use caching strategies like Cache-Control, ETag, and Last-Modified headers to improve performance by reducing server load and response times.

  • Filtering, Sorting, and Pagination: Implement these features to handle large datasets efficiently, allowing clients to retrieve targeted data without overwhelming the server.

  • Monitoring and Logging: Set up structured logging and real-time monitoring to track API performance, error rates, and traffic volume for better troubleshooting and optimization.

  • Comprehensive Documentation: Provide clear documentation with tools like Swagger or Postman, including endpoint descriptions, example requests/responses, and error codes for better developer experience.

Introduction

REST APIs (Representational State Transfer Application Programming Interfaces) are the backbone of modern web applications, enabling communication and data exchange between different systems in a scalable and flexible way. Whether you are building an API for a mobile app, a web service, or IoT devices, following best practices ensures that your API is secure, maintainable, and easy to use.

This comprehensive guide will cover the best practices for designing REST APIs, from naming conventions and security to performance optimization and error handling. 

What Are REST APIs?

A REST API is an architectural style for designing networked applications. It uses standard HTTP methods such as GET, POST, PUT, and DELETE to perform operations on resources identified by URIs (Uniform Resource Identifiers). REST APIs are stateless, meaning that each request from the client must contain all the information needed to process it—no client context is stored on the server between requests.

Some key characteristics of REST APIs are:

Statelessness: Each request is independent of others. The server does not store any session data.

Client-server architecture: The client and server are separate entities that communicate over HTTP.

Uniform interface: Resources are identified using URIs, and operations on these resources are performed using standard HTTP methods.

Scalability: REST APIs can scale horizontally by adding more servers because they are stateless.

With these characteristics in mind, let us explore the best practices for designing REST APIs.

Use Consistent Naming Conventions and URL Structure

Consistency in naming conventions and URL structure is essential for making your API intuitive and easy to use. A well-designed API should be predictable so that developers can easily understand how to interact with it. These are some guidelines for naming conventions:

1. Use Nouns Instead of Verbs

Endpoints should represent resources (nouns), not actions (verbs). For example:

  • GET /users/123/orders/456 (Correct)

  • GET /getUserOrders (Incorrect)

The first example correctly uses nouns (users and orders) to represent resources. The second example uses a verb (getUserOrders), which violates REST principles because it focuses on actions rather than resources.

2. Use Plural Nouns for Collections

When representing collections of resources, always use plural nouns:

  • GET /users (Correct)

  • GET /user (Incorrect)

Using plural nouns makes it clear that the endpoint represents a collection of items rather than a single resource.

3. Hierarchical Relationships

Use a hierarchical structure in URIs to represent relationships between resources:

  • GET /users/123/orders/456 (Correct)

  • GET /orders?userId=123&orderId=456 (Incorrect)

The first example follows a hierarchical structure where orders belong to users. The second example uses query parameters to represent relationships, which is less intuitive. However, when nesting relationships try to avoid going more than 3 levels deep as this can make the API less elegant and readable.

4. Lowercase URIs

Always use lowercase letters in URIs to avoid case-sensitivity issues.

  • `GET /categories/technology` (Correct)

  • `GET /Categories/Technology` (Incorrect)

5. Example in Express.js

This is an example in Express.js which combines all the naming conventions we talked about:

const express = require('express');
const app = express();
app.use(express.json());
app.get('/users/:id', (req, res) => {
    // Retrieve user by ID
    res.json({ id: req.params.id });
});
app.post('/users', (req, res) => {
    // Create new user
    res.status(201).json(req.body);
});
app.put('/users/:id', (req, res) => {
    // Update user by ID
    res.json(req.body);
});
app.delete('/users/:id', (req, res) => {
    // Delete user by ID
    res.status(204).send();
});
app.listen(3000);

In this example, we use consistent naming conventions (users, id) and follow RESTful principles by using nouns instead of verbs. We also ensure that each endpoint corresponds to a specific resource or collection of resources.

Use JSON for API Data Exchange

JSON (JavaScript Object Notation) is now the standard format for API requests and responses, replacing XML and HTML due to its simplicity and broad framework support. Unlike XML, JSON is easily parsed by JavaScript (e.g., via the fetch API) and is also supported across languages like Python (json.loads(), json.dumps()) and PHP.

To ensure clients interpret JSON correctly, set the Content-Type header to application/json. Most server-side frameworks, such as Express, handle this automatically with middleware like express.json() or the body-parser package.

Use HTTP Methods Correctly

In HTTP, methods have properties that determine how they should be used in REST APIs:

Safe: A method is considered safe if it does not alter server resources. For example, GET and HEAD requests retrieve data without making changes.

Idempotent: A method is idempotent if multiple identical requests produce the same result as a single request. PUT and DELETE are idempotent because repeating them does not alter the outcome.

Cacheable: A method is cacheable if its response can be stored and reused for future requests, enhancing efficiency. Only certain methods, like GET and HEAD, are cacheable by default.

HTTP methods define the type of operation you want to perform on a resource. Each method has a specific purpose in REST APIs:

GET: Retrieves data from the server, making it safe, idempotent, and cacheable.

POST: Sends data to create or update resources. POST is neither safe nor idempotent, as each request may create new resources.

PUT: Replaces or updates a resource, making it idempotent but not safe or cacheable.

DELETE: Removes a resource. It is idempotent but not safe, as it modifies server data.

HEAD: Retrieves headers only, without the resource body. It is safe, idempotent, and cacheable.

OPTIONS: Provides communication options for a resource. It is safe and idempotent, but not cacheable.

TRACE: Echoes the received request for diagnostics. It is safe and idempotent.

Statelessness

REST APIs must be stateless. This means that each request from the client must contain all necessary information for the server to process it. The server should not store any client context between requests.

Statelessness improves scalability because any server can handle any request without relying on session data stored on another server. This allows you to scale your API horizontally by adding more servers behind a load balancer. Hence, maintaining statelessness is very important for REST API design:

// Include authentication token in each request header:
GET /orders HTTP/1.1
Authorization: Bearer <token>

In this example, the client includes an authentication token with every request so that the server can authenticate and authorize the request without needing session data from previous requests.

Use Standard HTTP Status Codes

Using standard HTTP status codes helps clients understand the result of their requests without needing custom error messages or documentation. The status codes are grouped into five classes:

  • Informational responses (100 – 199)

  • Successful responses (200 – 299)

  • Redirection messages (300 – 399)

  • Client error responses (400 – 499)

  • Server error responses (500 – 599)

Commonly used status codes include:

200 OK: Successful GET or PUT request.

201 Created: Successful POST request.

204 No Content: Successful DELETE request with no content returned.

400 Bad Request: Invalid input from the client.

401 Unauthorized: Authentication required or failed.

403 Forbidden: Authorization required but denied.

404 Not Found: Resource not found.

500 Internal Server Error: Generic server error indicating something went wrong on the server side.

Check out these docs to see the full range of status codes which can be used in development.

In Express.js, these can be implemented like this:

app.post('/users', (req, res) => {
    const { email } = req.body;
    if (!email) {
        return res.status(400).json({ error: 'Email is required' });
    }
    // Create user logic here...
    res.status(201).json(req.body);
});

If the client sends an invalid request without an email field (400 Bad Request), we respond with an appropriate status code along with an error message. If everything goes well during user creation (201 Created), we return a success response along with the created user data.

Versioning Your API

APIs evolve over time as new features are added or existing ones are modified. To avoid breaking changes for existing clients when updating your API, it is important to implement versioning from day one.

Some common approaches to versioning are:

1. URI Versioning

Add version numbers directly into your URI paths.

2. Header Versioning

Specify version numbers in headers instead of URIs. This requires clients to manage headers and can be less approachable for those who are unfamiliar with them.



3. Query Parameter Versioning

Use query parameters to specify versions.

Overall, the most common approach is to include the version in the URI with semantic versioning. Some examples of semantic versioning are 1.0.0 and 2.1.3. These stand for MAJOR.MINOR.PATCH in terms of version numbers.

An example of implementing versioning:

app.get('/v1/users/:id', (req, res) => {
    // Logic for version 1 of the API...
});
app.get('/v2/users/:id', (req, res) => {
    // Logic for version 2 of the API...
});

In this, we have two versions of our API (v1 and v2). Clients can choose which version they want to interact with by specifying it in their requests.

Backward Compatibility

Backward compatibility ensures that existing clients using older versions of your API continue functioning even after you release updates or new versions. Breaking changes can disrupt clients' workflows and cause frustration among developers who rely on your API. Best practices for maintaining backward compatibility include:

Avoid Removing Fields: Retain all existing fields in responses unless absolutely necessary to maintain backward compatibility.

Gradual Deprecation: Deprecate endpoints gradually by providing clear warnings in advance of removal to allow clients time to adjust.

Versioning for Breaking Changes: Use proper versioning techniques for breaking changes, ensuring that older clients can continue to use previous versions without disruption.

By maintaining backward compatibility through proper versioning strategies and deprecation warnings, you ensure smooth transitions between different versions of your API while keeping existing clients happy.

Implement Filtering, Sorting, and Pagination

When dealing with large datasets in a REST API, it is inefficient and impractical to return all results at once. Instead, you should allow clients to retrieve subsets of data using filtering, sorting, and pagination. These features not only improve performance but also provide flexibility for clients to retrieve targeted data.

1. Filtering

Filtering allows clients to narrow down results by specifying certain criteria. For example, if you have a list of products, you may want to filter them by category, price range, or availability:

// GET /products?category=electronics&price_gte=100&price_lte=500
app.get('/products', (req, res) => {
    const { category, price_gte, price_lte } = req.query;
    let products = getProductsFromDatabase();
    if (category) {
        products = products.filter(p => p.category === category);
    }
    if (price_gte) {
        products = products.filter(p => p.price >= parseFloat(price_gte));
    }
    if (price_lte) {
        products = products.filter(p => p.price <= parseFloat(price_lte));
    }
    res.json(products);
});

In this, the API allows clients to filter products by category and price range using query parameters (category, price_gte, price_lte). The server processes these filters and returns only the matching results.

2. Sorting

Sorting enables clients to order the results based on one or more fields. For example, you might want to sort products by price in ascending order or by name alphabetically:

// GET /products?sort_by=price&sort_order=asc
app.get('/products', (req, res) => {
    const { sort_by = 'name', sort_order = 'asc' } = req.query;
    let products = getProductsFromDatabase();
    // Sort the products based on the query parameters
    products.sort((a, b) => {
        if (sort_order === 'asc') {
            return a[sort_by] > b[sort_by] ? 1 : -1;
        } else {
            return a[sort_by] < b[sort_by] ? 1 : -1;
        }
    });
    res.json(products);
});

This API allows sorting by any field (sort_by), such as price or name, and supports both ascending (asc) and descending (desc) order. The server sorts the results before returning them to the client.

3. Pagination

Pagination is essential for limiting the number of results returned in a single response. This prevents overwhelming the server and client with too much data at once. Clients can request specific pages of data using query parameters like page and limit.

// GET /products?page=2&limit=10
app.get('/products', (req, res) => {
    const { page = 1, limit = 10 } = req.query;
    let products = getProductsFromDatabase();
    // Calculate pagination
    const startIndex = (page - 1) * limit;
    const endIndex = startIndex + parseInt(limit);
    const paginatedProducts = products.slice(startIndex, endIndex);
    res.json({
        page: parseInt(page),
        limit: parseInt(limit),
        total: products.length,
        data: paginatedProducts,
    });
});

The API uses page and limit query parameters to paginate the results. The server calculates which subset of data to return based on the requested page and limit. Metadata such as the total number of records is included in the response for better client-side handling.

For APIs dealing with large datasets where performance is critical, keyset pagination (also known as seek pagination) can be used instead of traditional offset-based pagination. Keyset pagination ensures that queries remain performant even when dealing with millions of records.

// GET /products?limit=20&after_id=100
app.get('/products', (req, res) => {
    const { limit = 20, after_id } = req.query;
    let products = getProductsFromDatabase();
    // Apply keyset pagination logic
    if (after_id) {
        products = products.filter(p => p.id > after_id);
    }
    const paginatedProducts = products.slice(0, parseInt(limit));
    res.json({
        limit: parseInt(limit),
        data: paginatedProducts,
        last_id: paginatedProducts[paginatedProducts.length - 1]?.id,
    });
});

Through this, clients can request a specific number of items (limit) after a certain ID (after_id), which improves performance when dealing with large datasets.

Keyset pagination avoids performance issues associated with large offsets in traditional pagination.

For more pagination strategies, refer to this article.

Caching Responses

Caching improves performance by reducing server load and response times for frequently requested data. Use HTTP headers like Cache-Control, ETag, and Last-Modified to manage caching effectively. In Express.js, the apicache middleware can be used to cache data:

const apicache = require('apicache');
let cache = apicache.middleware;
app.use(cache('5 minutes'));
app.get('/products', (req, res) => {
    // Fetch products logic...
    res.json(products);
});

Security Best Practices

Security is one of the most critical aspects of designing REST APIs. APIs often expose sensitive data or functionality that malicious actors could exploit. To secure your API effectively, you need to implement several best practices related to authentication, authorization, input validation, and more.

1. Use HTTPS

Always use SSL/TLS (Secure Sockets Layer and its successor Transport Layer Security) encryption via HTTPS to secure communication between clients and servers and avoid man-in-the-middle attacks. These require certificates issued by a certificate authority, also letting users know that the API is protected. Most of these certificates are not hard to load onto a server and are worth the security they provide.

2. DTOs (Data Transfer Objects)

Using Data Transfer Objects (DTOs) is a security best practice that helps protect your internal models from being exposed directly through your API. DTOs act as an intermediary between your internal data structures and the data sent over the network. By using DTOs, you can control exactly what data is exposed to clients and ensure that sensitive information remains hidden.

// User model
class User {
  constructor(id, username, passwordHash) {
      this.id = id;
      this.username = username;
      this.passwordHash = passwordHash; // Sensitive information!
  }
}
// User DTO (Data Transfer Object)
class UserDTO {
  constructor(id, username) {
      this.id = id;
      this.username = username; // Expose only non-sensitive fields
  }
}
// Function to convert User model to UserDTO
function toUserDTO(user) {
  return new UserDTO(user.id, user.username);
}
// API endpoint using DTOs
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  const user = getUserFromDatabase(userId); // Assume this function fetches user from DB
  if (!user) {
      return res.status(404).json({ error: 'User not found' });
  }
  const userDTO = toUserDTO(user); // Convert User model to UserDTO before sending response
  res.json(userDTO);
});

In this example, the internal User model contains sensitive information such as passwordHash, which should never be exposed through an API. The UserDTO class defines only the fields that are safe for public consumption. Before returning user data from the API endpoint, we convert the internal User object into a UserDTO, ensuring that sensitive fields are excluded from the response.

3. Authentication with OAuth2 or JWT

Authentication ensures that only authorized users can access your API. Two common approaches are OAuth2 and JWT (JSON Web Tokens). The following is an example implementation of JWTs:

const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
    const token = req.header('Authorization')?.split(' ')[1];
    if (!token) return res.status(401).send('Access Denied');
    jwt.verify(token, process.env.TOKEN_SECRET, (err, user) => {
        if (err) return res.status(403).send('Invalid Token');
        req.user = user;
        next();
    });
}
app.get('/protected-route', authenticateToken, (req, res) => {
   res.send('This is a protected route');
});

JWT tokens are used for stateless authentication. Each request must include a valid token in its headers. The server verifies the token before allowing access to protected resources.

4. Input Validation

Always validate and sanitize user inputs to prevent attacks like SQL injection or cross-site scripting (XSS). Use libraries like express-validator for input validation in Express.js:

const { body } = require('express-validator');
app.post('/users', [
  body('email').isEmail(),
  body('password').isLength({ min: 6 })
], (req, res) => {
  const errors = validationResult(req);
  
  if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
  }
  // Proceed with creating the user...
});

In this, we validate that the email field contains a valid email address and that the password is at least six characters long. If validation fails, we return a 400 Bad Request response with detailed error messages.

5. Rate Limiting

Implement rate limiting to prevent abuse such as Denial-of-Service attacks. Rate limiting can be implemented through various strategies such as:

Fixed Window: Allows a set number of requests within a specific timeframe, resetting after each period (e.g., 100 requests per minute).

Sliding Window: Monitors requests over a continuous time window, rather than resetting at fixed intervals (e.g., 15 requests every 10 minutes with no hard reset).

Token Bucket: Clients receive tokens representing allowed requests, with tokens replenished at a set rate (e.g., starting with 10 tokens and gaining 1 token per second).

Limit Concurrency: Restricts the number of simultaneous requests, queuing or denying additional ones based on the policy (e.g., a client can only execute 2 requests at a time).

The following is an example using express-rate-limit middleware:

const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100 // Limit each IP to 100 requests per windowMs 
});
app.use(limiter);

6. Enforcing the Principle of Least Privilege

Limit access strictly to the information requested. For instance, a regular user should not have access to another user’s data or to admin-level information. API clients do not need to be given unnecessary access or permissions.

To apply the principle of least privilege, role-based checks should be implemented. This can be done by assigning users either a single role or by defining more granular roles based on each user's access needs.

If grouping users by roles, ensure roles have permissions that cover only necessary tasks—no excess permissions. For more granular permission settings, admins should be able to manage features for each user, adding or removing access as needed. Additionally, create preset roles for user groups to simplify role assignment across multiple users.

Monitoring and Logging

Effective monitoring and logging of API requests and responses provide essential insights into performance, usage, and potential issues, especially in production environments. Dedicated API monitoring tools such as New Relic, Datadog, Prometheus with Grafana, AWS CloudWatch, Elastic APM, and Apigee offer robust solutions for this purpose.

For successful monitoring, follow these logging best practices:

Structured Logging: Use structured formats like JSON to make logs easily searchable and compatible with monitoring tools.

Log Levels: Apply log levels (e.g., INFO, WARN, ERROR) to classify log entries by severity.

Centralized Logging: Aggregate logs from multiple sources with solutions like ELK Stack or Splunk.

Real-Time Monitoring: Implement real-time tracking to monitor API health and receive alerts for immediate issue detection. Key metrics such as error rates (500 Internal Server Errors), traffic volume per endpoint, and average response times per route should be monitored.

Log Retention Policies: Establish policies that balance storage costs with the need for historical data for trend analysis and troubleshooting.

In Express.js, the morgan middleware can be used for logging HTTP requests and errors.

Provide Comprehensive Documentation

Good documentation is critical for API adoption. Tools like Swagger or Postman can help generate interactive documentation automatically from your API definitions. Key elements of good documentation include endpoint descriptions, example requests and responses, error codes and their meanings, and implementation in several languages if possible.

Conclusion

Following these best practices ensures that your REST API is scalable, secure, and easy to maintain. By adhering to consistent naming conventions and versioning strategies, using appropriate HTTP methods and status codes, and implementing security measures like OAuth2/JWT authentication and HTTPS encryption, you can build an API that developers will love to use. Additionally, performance optimizations like caching and rate limiting ensure that your API remains responsive even under heavy load.

By combining these practices with proper versioning strategies and comprehensive documentation tools like Swagger or Postman, you can ensure that your API remains future-proof while providing an excellent developer experience.

Looking to build a reliable, high-performing API?

Our experienced developers at Walturn follow all the best practices for REST APIs to ensure your product is secure, scalable, and efficient. Let’s bring your project to life with industry-leading standards—partner with Walturn today!

References

Best Practices for REST API Design - Stack Overflow. 2 Mar. 2020, stackoverflow.blog/2020/03/02/best-practices-for-rest-api-design.

Best Practices for REST API Security: Authentication and Authorization - Stack Overflow. 6 Oct. 2021, stackoverflow.blog/2021/10/06/best-practices-for-authentication-and-authorization-for-rest-apis.

Chris, Kolade. “REST API Best Practices – REST Endpoint Design Examples.” freeCodeCamp.org, 16 Sept. 2021, www.freecodecamp.org/news/rest-api-best-practices-rest-endpoint-design-examples.

Gupta, Lokesh. “REST API Best Practices.” REST API Tutorial, 23 Oct. 2024, restfulapi.net/rest-api-best-practices.

“HTTP Response Status Codes - HTTP | MDN.” MDN Web Docs, 18 Oct. 2024, developer.mozilla.org/en-US/docs/Web/HTTP/Status.

Rahman, Md Mostafizur. “Simplifying DTO Management in Express.js With Class-Transformer.” DEV Community, 5 Mar. 2023, dev.to/mdmostafizurrahaman/simplifying-dto-management-in-expressjs-with-class-transformer-56mh.

Schneidenbach, Spencer. “RESTful API Best Practices and Common Pitfalls - Spencer Schneidenbach - Medium.” Medium, 22 Jan. 2020, medium.com/@schneidenbach/restful-api-best-practices-and-common-pitfalls-7a83ba3763b5.

Verma, Pragati. “Unlocking the Power of API Pagination: Best Practices and Strategies.” DEV Community, 6 June 2023, dev.to/pragativerma18/unlocking-the-power-of-api-pagination-best-practices-and-strategies-4b49.

Other Insights

Got an app?

We build and deliver stunning mobile products that scale

Got an app?

We build and deliver stunning mobile products that scale

Got an app?

We build and deliver stunning mobile products that scale

Got an app?

We build and deliver stunning mobile products that scale

Got an app?

We build and deliver stunning mobile products that scale

Our mission is to harness the power of technology to make this world a better place. We provide thoughtful software solutions and consultancy that enhance growth and productivity.

The Jacx Office: 16-120

2807 Jackson Ave

Queens NY 11101, United States

Book an onsite meeting or request a services?

© Walturn LLC • All Rights Reserved 2024

Our mission is to harness the power of technology to make this world a better place. We provide thoughtful software solutions and consultancy that enhance growth and productivity.

The Jacx Office: 16-120

2807 Jackson Ave

Queens NY 11101, United States

Book an onsite meeting or request a services?

© Walturn LLC • All Rights Reserved 2024

Our mission is to harness the power of technology to make this world a better place. We provide thoughtful software solutions and consultancy that enhance growth and productivity.

The Jacx Office: 16-120

2807 Jackson Ave

Queens NY 11101, United States

Book an onsite meeting or request a services?