DevToolBoxฟรี
บล็อก

REST API Best Practices: คู่มือฉบับสมบูรณ์ 2026

15 นาทีในการอ่านโดย DevToolBox

Building a well-designed REST API is one of the most important skills for modern developers. A great API is intuitive, consistent, secure, and easy to maintain. This comprehensive guide covers everything from URL naming conventions and HTTP methods to authentication, pagination, rate limiting, and common mistakes. Whether you are building your first API or refining an existing one, these best practices will help you create APIs that developers love to use.

URL Naming Conventions

The foundation of a good REST API starts with well-designed URLs. Your endpoints should be predictable, readable, and follow consistent patterns.

Use Nouns, Not Verbs

REST resources should be identified by nouns. The HTTP method already indicates the action, so there is no need to include verbs in the URL path. Think of URLs as addresses to resources, not commands.

# Good - nouns as resources
GET    /users          # Get all users
GET    /users/123      # Get user 123
POST   /users          # Create a new user
DELETE /users/123      # Delete user 123

# Bad - verbs in URLs
GET    /getUsers
POST   /createUser
DELETE /deleteUser/123

Use Plurals for Collections

Always use plural nouns for collection endpoints. This maintains consistency whether you are referencing a collection or a single resource within it.

# Good - consistent plurals
GET /users          # Collection
GET /users/123      # Single resource in collection
GET /products       # Collection
GET /products/456   # Single resource

# Bad - inconsistent or singular
GET /user           # Ambiguous: one user or all users?
GET /user/123
GET /product-list

Nested Resources for Relationships

Use nesting to express relationships between resources, but avoid going deeper than two levels. Deep nesting makes URLs hard to read and maintain.

# Good - clear parent-child relationship (max 2 levels)
GET /users/123/orders          # Orders belonging to user 123
GET /users/123/orders/456      # Specific order of user 123

# Bad - too deeply nested
GET /users/123/orders/456/items/789/reviews
# Better: use a direct resource endpoint
GET /order-items/789/reviews
# Or: flatten with query parameters
GET /reviews?order_item_id=789

URL Patterns: Good vs Bad

GoodBadReason
GET /usersGET /getUsersHTTP method already implies the action
GET /users/123GET /user?id=123Use path parameters for resource identity
POST /usersPOST /createUserPOST already means create
PUT /users/123POST /updateUserPUT/PATCH for updates, use resource path
DELETE /users/123POST /deleteUserUse DELETE method, not POST with verb
GET /users/123/ordersGET /getUserOrders?userId=123Use nested resources for relationships
GET /products?category=electronicsGET /electronics/productsUse query params for filtering
GET /users?status=active&sort=nameGET /active-users-sorted-by-nameUse query params, not encoded URLs
PATCH /users/123POST /users/123/updateUse HTTP methods, not URL verbs
GET /users/123/avatarGET /getAvatarForUser/123Keep it resource-oriented

HTTP Methods

Each HTTP method has specific semantics. Using them correctly makes your API predictable and allows clients to understand the intent of each request without reading documentation.

MethodPurposeIdempotentRequest BodyExample
GETRetrieve a resource or collectionYesNoGET /users/123
POSTCreate a new resourceNoYesPOST /users
PUTReplace a resource entirelyYesYesPUT /users/123
PATCHPartially update a resourceNo*YesPATCH /users/123
DELETERemove a resourceYesOptionalDELETE /users/123

* PATCH can be idempotent depending on implementation. If the patch describes the final state (e.g., {"name": "John"}), it is idempotent. If it describes an operation (e.g., {"op": "increment", "path": "/count"}), it is not.

HTTP Status Codes

Using the correct status codes helps clients handle responses appropriately. Here are the most important status codes for REST APIs and when to use each one.

CodeNameWhen to Use
200OKSuccessful GET, PUT, PATCH, or DELETE. Return the resource or confirmation.
201CreatedSuccessful POST that created a new resource. Include Location header with the new resource URL.
204No ContentSuccessful DELETE or PUT/PATCH when no response body is needed.
400Bad RequestThe request is malformed — invalid JSON, missing required fields, wrong data types.
401UnauthorizedNo valid authentication credentials provided. The client must authenticate first.
403ForbiddenThe client is authenticated but does not have permission for this action.
404Not FoundThe requested resource does not exist. Also use for hidden resources (instead of 403) for security.
409ConflictThe request conflicts with the current state — duplicate email, version mismatch, etc.
422Unprocessable EntityThe request is well-formed but fails validation — email format invalid, password too short.
429Too Many RequestsRate limit exceeded. Include Retry-After header with seconds until the client can retry.
500Internal Server ErrorAn unexpected server error occurred. Log the details server-side but do not expose them to clients.
503Service UnavailableThe server is temporarily unavailable — maintenance mode, overloaded. Include Retry-After header.

Error Handling

Consistent error responses make your API easier to consume. Every error should return a structured JSON body with enough information for the client to understand and fix the problem.

Standard Error Response Format

Use a consistent error envelope for all error responses. Include a machine-readable error code, a human-readable message, and optional details.

// Standard error response
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "The user with ID 123 was not found.",
    "status": 404,
    "timestamp": "2026-01-15T10:30:00Z",
    "request_id": "req_abc123def456",
    "documentation_url": "https://api.example.com/docs/errors#RESOURCE_NOT_FOUND"
  }
}

// Another example: authentication error
{
  "error": {
    "code": "TOKEN_EXPIRED",
    "message": "The access token has expired. Please refresh your token.",
    "status": 401,
    "timestamp": "2026-01-15T10:30:00Z",
    "request_id": "req_xyz789"
  }
}

Validation Errors with Field-Level Details

For 422 validation errors, include field-level error information so clients can display errors next to the correct form fields.

// 422 Validation error with field-level details
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The request body contains invalid fields.",
    "status": 422,
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address.",
        "rejected_value": "not-an-email"
      },
      {
        "field": "password",
        "message": "Must be at least 8 characters long.",
        "rejected_value": "short"
      },
      {
        "field": "age",
        "message": "Must be a positive integer.",
        "rejected_value": -5
      }
    ],
    "timestamp": "2026-01-15T10:30:00Z",
    "request_id": "req_val456"
  }
}

Authentication & Authorization

Choosing the right authentication strategy depends on your use case. Here is a comparison of the three most common approaches.

API Keys

Simple string tokens passed in headers or query parameters. Best for server-to-server communication and simple integrations. Easy to implement but offer limited security since keys are long-lived and hard to scope.

# API Key in header (recommended)
GET /api/users HTTP/1.1
Host: api.example.com
X-API-Key: sk_live_abc123def456ghi789

# API Key in query parameter (less secure - visible in logs)
GET /api/users?api_key=sk_live_abc123def456ghi789

JWT Bearer Tokens

Self-contained tokens that carry user claims. The server can verify the token without a database lookup. Ideal for stateless authentication in microservices. Tokens should be short-lived with refresh token rotation.

# JWT Bearer Token in Authorization header
GET /api/users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

# Token refresh flow
POST /api/auth/refresh HTTP/1.1
Content-Type: application/json
{
  "refresh_token": "rt_abc123def456"
}

# Response
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt_new789xyz"
}

OAuth 2.0

The industry standard for delegated authorization. Allows third-party applications to access resources on behalf of a user without exposing credentials. More complex to implement but provides the most flexibility and security.

# OAuth 2.0 Authorization Code Flow

# Step 1: Redirect user to authorization server
GET https://auth.example.com/authorize?
  response_type=code&
  client_id=your_client_id&
  redirect_uri=https://yourapp.com/callback&
  scope=read:users write:users&
  state=random_csrf_token

# Step 2: Exchange authorization code for tokens
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AUTH_CODE_FROM_CALLBACK&
client_id=your_client_id&
client_secret=your_client_secret&
redirect_uri=https://yourapp.com/callback

# Step 3: Use the access token
GET /api/users/me HTTP/1.1
Authorization: Bearer ACCESS_TOKEN_HERE

Authentication Comparison

FeatureAPI KeyJWTOAuth 2.0
ComplexityLowMediumHigh
StatelessYesYesDepends
User contextNoYesYes
Token expirationManual rotationBuilt-in (exp claim)Built-in
Scope/permissionsLimitedVia claimsFine-grained scopes
Third-party accessNoNoYes (delegated)
Best forServer-to-serverSPAs, Mobile appsThird-party integrations
RevocationDelete keyBlacklist neededRevoke at auth server

Pagination Patterns

Any endpoint that returns a list of resources should support pagination. Without it, responses can become enormous and slow as your data grows.

Offset-Based Pagination

The simplest approach. Uses page number and limit parameters. Easy to implement and allows jumping to any page, but can be inconsistent when data is modified between requests.

# Offset-based pagination request
GET /api/users?page=2&limit=20

# How it works internally
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 20;  -- Skip first 20, return next 20

# Pros: Simple, allows jumping to any page
# Cons: Inconsistent with concurrent modifications,
#        slow on large offsets (OFFSET 100000)

Cursor-Based Pagination

Uses an opaque cursor token to mark the position in the dataset. More reliable for real-time data and large datasets because it does not skip or duplicate items when data changes.

# Cursor-based pagination request
GET /api/users?cursor=eyJpZCI6MTIzfQ&limit=20

# The cursor is an opaque token (often base64-encoded)
# that points to a specific position in the dataset

# How it works internally (cursor decodes to {"id": 123})
SELECT * FROM users
WHERE id > 123
ORDER BY id ASC
LIMIT 20;

# Pros: Consistent results, fast on large datasets
# Cons: Cannot jump to arbitrary pages,
#        only next/previous navigation

Pagination Response Format

Include metadata about the pagination state in every paginated response. This allows clients to build navigation controls.

// Offset-based pagination response
{
  "data": [
    { "id": 21, "name": "Alice" },
    { "id": 22, "name": "Bob" }
    // ... 18 more items
  ],
  "meta": {
    "current_page": 2,
    "per_page": 20,
    "total_pages": 15,
    "total_count": 294
  },
  "links": {
    "self": "/api/users?page=2&limit=20",
    "first": "/api/users?page=1&limit=20",
    "prev": "/api/users?page=1&limit=20",
    "next": "/api/users?page=3&limit=20",
    "last": "/api/users?page=15&limit=20"
  }
}

// Cursor-based pagination response
{
  "data": [
    { "id": 124, "name": "Charlie" },
    { "id": 125, "name": "Diana" }
    // ... 18 more items
  ],
  "meta": {
    "has_more": true,
    "next_cursor": "eyJpZCI6MTQzfQ",
    "prev_cursor": "eyJpZCI6MTI0fQ"
  },
  "links": {
    "next": "/api/users?cursor=eyJpZCI6MTQzfQ&limit=20",
    "prev": "/api/users?cursor=eyJpZCI6MTI0fQ&limit=20&direction=prev"
  }
}

API Versioning Strategies

APIs evolve over time. Versioning allows you to make breaking changes without disrupting existing clients. Here are the three most common approaches.

StrategyExampleProsCons
URL Path/v1/users, /v2/usersExplicit, easy to route and cache, clear in logs and docsURL pollution, harder to sunset old versions
Custom HeaderAccept-Version: v2 or X-API-Version: 2Clean URLs, version hidden from casual usersHarder to test (need tools to set headers), not cacheable by default
Query Parameter/users?version=2Easy to test in browser, no URL path changesCan be accidentally omitted, harder to route, pollutes query string
Content NegotiationAccept: application/vnd.api.v2+jsonMost RESTful approach, per-resource versioningComplex, hard to test, poorly supported by many tools
# URL Path versioning (recommended)
GET /v1/users/123        # Version 1
GET /v2/users/123        # Version 2 with breaking changes

# Express.js router example
const v1Router = express.Router();
const v2Router = express.Router();

v1Router.get('/users/:id', v1UserController.get);
v2Router.get('/users/:id', v2UserController.get);

app.use('/v1', v1Router);
app.use('/v2', v2Router);

Rate Limiting

Rate limiting protects your API from abuse and ensures fair usage across all clients. Always communicate rate limit status through response headers.

Rate Limit Headers

Include these headers in every response so clients can monitor their usage and avoid hitting limits.

HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 1000          # Max requests per window
X-RateLimit-Remaining: 847       # Remaining requests in window
X-RateLimit-Reset: 1706176800    # Unix timestamp when window resets
X-RateLimit-Window: 3600         # Window duration in seconds (1 hour)

# Some APIs also include:
RateLimit-Policy: 1000;w=3600    # IETF draft standard format

429 Too Many Requests Response

When a client exceeds the rate limit, return a 429 status with a Retry-After header indicating when they can make requests again.

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 45
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706176800

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "You have exceeded the rate limit of 1000 requests per hour.",
    "status": 429,
    "retry_after": 45,
    "documentation_url": "https://api.example.com/docs/rate-limiting"
  }
}

Backoff Strategies

Clients should implement exponential backoff when they receive 429 responses. Start with a short delay and double it with each retry, up to a maximum.

// Exponential backoff with jitter
async function fetchWithRetry(url, options, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status !== 429) {
      return response;
    }

    // Use Retry-After header if available
    const retryAfter = response.headers.get('Retry-After');
    let delay;

    if (retryAfter) {
      delay = parseInt(retryAfter, 10) * 1000;
    } else {
      // Exponential backoff: 1s, 2s, 4s, 8s, 16s
      const baseDelay = Math.pow(2, attempt) * 1000;
      // Add jitter (random 0-1s) to prevent thundering herd
      const jitter = Math.random() * 1000;
      delay = baseDelay + jitter;
    }

    console.log(`Rate limited. Retrying in ${delay}ms (attempt ${attempt + 1})`);
    await new Promise(resolve => setTimeout(resolve, delay));
  }

  throw new Error('Max retries exceeded');
}

Security Headers

Security headers protect your API from common web vulnerabilities. These headers should be included in every response from your API.

HeaderRecommended ValuePurpose
Access-Control-Allow-Originhttps://yourapp.com (specific origin)CORS: Controls which domains can call your API. Never use * in production with credentials.
Access-Control-Allow-MethodsGET, POST, PUT, PATCH, DELETECORS: Specifies which HTTP methods are allowed for cross-origin requests.
Access-Control-Allow-HeadersAuthorization, Content-TypeCORS: Specifies which request headers are allowed in cross-origin requests.
Strict-Transport-Securitymax-age=31536000; includeSubDomainsHSTS: Forces browsers to use HTTPS for all future requests to your domain.
Content-Security-Policydefault-src 'none'; frame-ancestors 'none'CSP: Prevents XSS and data injection attacks. For APIs, restrict everything.
X-Content-Type-OptionsnosniffPrevents browsers from MIME-type sniffing. Ensures responses are treated as their declared content type.
X-Frame-OptionsDENYPrevents your API responses from being embedded in iframes (clickjacking protection).
Cache-Controlno-store, no-cache, must-revalidatePrevents caching of sensitive API responses. Adjust per endpoint as needed.
// Express.js security headers middleware
const helmet = require('helmet');

app.use(helmet());

// Custom CORS configuration
const cors = require('cors');
app.use(cors({
  origin: ['https://yourapp.com', 'https://admin.yourapp.com'],
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Authorization', 'Content-Type'],
  credentials: true,
  maxAge: 86400  // Cache preflight for 24 hours
}));

// Additional security headers
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('Cache-Control', 'no-store');
  next();
});

API Documentation with OpenAPI

Good documentation is essential for API adoption. OpenAPI (formerly Swagger) is the standard for describing REST APIs. It enables automatic generation of documentation, client SDKs, and testing tools.

  • Write the OpenAPI spec first (design-first approach), then implement the API to match the spec.
  • Include detailed descriptions for every endpoint, parameter, and response schema.
  • Provide realistic example values for all request and response bodies.
  • Document error responses for every endpoint, not just success cases.
  • Use tags to group related endpoints and make the documentation navigable.
  • Version your OpenAPI spec alongside your API code.
  • Set up interactive documentation (Swagger UI or Redoc) so developers can try requests directly.
# OpenAPI 3.1 specification example
openapi: 3.1.0
info:
  title: User Management API
  version: 1.0.0
  description: API for managing users and their resources

servers:
  - url: https://api.example.com/v1

paths:
  /users:
    get:
      summary: List all users
      tags: [Users]
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        '200':
          description: A paginated list of users
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'
              example:
                data:
                  - id: 1
                    name: "Alice Johnson"
                    email: "alice@example.com"
                meta:
                  current_page: 1
                  total_pages: 10
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      summary: Create a new user
      tags: [Users]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
            example:
              name: "Bob Smith"
              email: "bob@example.com"
              password: "securePassword123"
      responses:
        '201':
          description: User created successfully
        '422':
          $ref: '#/components/responses/ValidationError'

Common REST API Mistakes

Even experienced developers make these mistakes. Review this list to ensure your API follows best practices.

#MistakeWhy It Is WrongFix
1Using verbs in URLsREST is resource-oriented; HTTP methods express actionsUse nouns: /users not /getUsers
2Returning 200 for errorsClients rely on status codes for error handlingUse proper status codes: 400, 404, 422, 500
3Not paginating list endpointsReturns grow unbounded, causing timeouts and memory issuesAlways paginate with sensible defaults (limit=20)
4Exposing internal errorsStack traces reveal implementation details to attackersLog details server-side, return generic error messages
5Ignoring CORS headersBrowser-based clients cannot call your APIConfigure CORS for allowed origins and methods
6Not versioning the APIBreaking changes affect all clients simultaneouslyUse URL path versioning: /v1/users
7Using POST for everythingLoses HTTP method semantics, breaks caching and idempotencyUse GET, POST, PUT, PATCH, DELETE appropriately
8Inconsistent naming conventionsMixed case or formats confuse API consumersPick one (snake_case or camelCase) and be consistent
9No rate limitingAPI is vulnerable to abuse and DDoS attacksImplement rate limiting with clear headers
10Missing Content-Type headerClients cannot parse responses correctlyAlways set Content-Type: application/json

Try these related developer tools

Frequently Asked Questions

What is the difference between REST and RESTful?

REST (Representational State Transfer) is an architectural style defined by Roy Fielding. RESTful is an adjective describing APIs that adhere to REST constraints. In practice, most "REST APIs" do not strictly follow all REST constraints (like HATEOAS), but the terms are used interchangeably.

Should I use PUT or PATCH for updating resources?

Use PUT when the client sends a complete replacement of the resource. Use PATCH for partial updates where only the changed fields are sent. PATCH is more bandwidth-efficient for large resources. Most modern APIs prefer PATCH for typical update operations.

How should I handle API versioning?

URL path versioning (e.g., /v1/users) is the most common and explicit approach. It is easy to understand, route, and cache. Header versioning is cleaner but harder to test and debug. Choose one strategy and apply it consistently across your entire API.

What is the best way to handle authentication in REST APIs?

For most applications, JWT Bearer tokens provide a good balance of security and simplicity. Use short-lived access tokens (15-60 minutes) with refresh token rotation. For third-party integrations, use OAuth 2.0. API keys are acceptable for server-to-server communication only.

How do I design nested resources in REST APIs?

Use nesting to show parent-child relationships: /users/123/orders. Limit nesting to two levels maximum. For deeper relationships, use query parameters or provide direct resource endpoints. For example, instead of /users/123/orders/456/items/789, use /order-items/789 or /orders/456/items/789.

Should REST API responses include null fields or omit them?

It depends on your contract. Including null fields makes the schema predictable - clients always know what fields to expect. Omitting null fields reduces payload size. Be consistent: pick one approach and document it. Many APIs include null for known fields and omit truly optional ones.

𝕏 Twitterin LinkedIn
บทความนี้มีประโยชน์ไหม?

อัปเดตข่าวสาร

รับเคล็ดลับการพัฒนาและเครื่องมือใหม่ทุกสัปดาห์

ไม่มีสแปม ยกเลิกได้ตลอดเวลา

ลองเครื่องมือที่เกี่ยวข้อง

4xxHTTP Status Code Reference>>cURL to Code ConverterJWTJWT Decoder{ }JSON Formatter

บทความที่เกี่ยวข้อง

HTTP Status Codes: คู่มืออ้างอิงฉบับสมบูรณ์สำหรับนักพัฒนา

คู่มือรหัสสถานะ HTTP: 1xx ถึง 5xx พร้อมคำอธิบายเชิงปฏิบัติ แนวปฏิบัติ API และเคล็ดลับการ debug

API Authentication: OAuth 2.0 vs JWT vs API Key

เปรียบเทียบวิธีการยืนยันตัวตน API: OAuth 2.0, JWT Bearer token และ API key