DevToolBoxZA DARMO
Blog

Przewodnik projektowania REST API: Najlepsze praktyki 2026

16 minby DevToolBox

Why REST API Design Matters

A well-designed REST API is intuitive, consistent, and easy to integrate. Poor API design leads to confused developers, increased support costs, breaking changes, and ultimately slower adoption. Whether you are building a public API, a microservices backend, or an internal service, following established design conventions saves time and reduces friction. This guide covers the essential best practices for designing REST APIs in 2026 with practical examples you can apply immediately. For related content, see our REST API Best Practices guide.

Resource Naming Conventions

Resources are the fundamental concept in REST. They represent the entities in your system. Resource URIs should be nouns (not verbs), use plural forms, and follow a consistent naming scheme. Use the Slug Generator to create clean URL segments.

# Good: Noun-based, plural, hierarchical
GET    /api/v1/users
GET    /api/v1/users/123
GET    /api/v1/users/123/orders
GET    /api/v1/users/123/orders/456
POST   /api/v1/users
PUT    /api/v1/users/123
PATCH  /api/v1/users/123
DELETE /api/v1/users/123

# Bad: Verb-based, inconsistent
GET    /api/v1/getUsers
POST   /api/v1/createUser
GET    /api/v1/user/123/getOrders
POST   /api/v1/deleteUser/123

# Resource naming rules:
# 1. Use plural nouns:          /users not /user
# 2. Use kebab-case:            /order-items not /orderItems
# 3. Use lowercase:             /users not /Users
# 4. Nest for relationships:    /users/123/posts
# 5. Max 3 levels of nesting:   /users/123/posts/456

HTTP Methods and Their Semantics

Each HTTP method has specific semantics. Using them correctly makes your API predictable and self-documenting. Check our HTTP Status Codes reference for response codes to pair with each method.

MethodActionIdempotentRequest BodySuccess Code
GETRead resource(s)YesNo200 OK
POSTCreate resourceNoYes201 Created
PUTFull replaceYesYes200 OK
PATCHPartial updateNo*Yes200 OK
DELETERemove resourceYesNo204 No Content
HEADGet headers onlyYesNo200 OK
OPTIONSList allowed methodsYesNo204 No Content

Request and Response Design

Request Body Structure

// POST /api/v1/users — Create a new user
// Request:
{
  "name": "Alice Johnson",
  "email": "alice@example.com",
  "role": "admin",
  "preferences": {
    "language": "en",
    "timezone": "America/New_York",
    "notifications": {
      "email": true,
      "sms": false
    }
  }
}

// Response (201 Created):
{
  "data": {
    "id": "usr_abc123",
    "name": "Alice Johnson",
    "email": "alice@example.com",
    "role": "admin",
    "preferences": {
      "language": "en",
      "timezone": "America/New_York",
      "notifications": {
        "email": true,
        "sms": false
      }
    },
    "createdAt": "2026-02-22T10:30:00Z",
    "updatedAt": "2026-02-22T10:30:00Z"
  }
}

Consistent Response Envelope

// Standard response format for all endpoints
interface ApiResponse<T> {
  data: T;
  meta?: {
    page: number;
    perPage: number;
    total: number;
    totalPages: number;
  };
  links?: {
    self: string;
    first?: string;
    prev?: string;
    next?: string;
    last?: string;
  };
}

interface ErrorResponse {
  error: {
    code: string;
    message: string;
    details?: Array<{
      field: string;
      message: string;
      code: string;
    }>;
    requestId: string;
    documentation?: string;
  };
}

// GET /api/v1/users?page=2&per_page=20
// Response (200 OK):
{
  "data": [
    { "id": "usr_abc123", "name": "Alice", "email": "alice@example.com" },
    { "id": "usr_def456", "name": "Bob", "email": "bob@example.com" }
  ],
  "meta": {
    "page": 2,
    "perPage": 20,
    "total": 145,
    "totalPages": 8
  },
  "links": {
    "self": "/api/v1/users?page=2&per_page=20",
    "first": "/api/v1/users?page=1&per_page=20",
    "prev": "/api/v1/users?page=1&per_page=20",
    "next": "/api/v1/users?page=3&per_page=20",
    "last": "/api/v1/users?page=8&per_page=20"
  }
}

Error Handling

Consistent error responses are critical for developer experience. Always include an error code, human-readable message, and enough detail for the client to fix the problem.

// 400 Bad Request — Validation error
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address",
        "code": "INVALID_FORMAT"
      },
      {
        "field": "age",
        "message": "Must be between 18 and 120",
        "code": "OUT_OF_RANGE"
      }
    ],
    "requestId": "req_xyz789"
  }
}

// 401 Unauthorized
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or expired authentication token",
    "documentation": "https://api.example.com/docs/authentication",
    "requestId": "req_xyz790"
  }
}

// 404 Not Found
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "User with ID 'usr_invalid' not found",
    "requestId": "req_xyz791"
  }
}

// 429 Too Many Requests
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Try again in 30 seconds.",
    "requestId": "req_xyz792"
  }
}

// 500 Internal Server Error
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "An unexpected error occurred",
    "requestId": "req_xyz793"
  }
}

Pagination

Always paginate list endpoints. Support multiple pagination strategies based on your use case.

# Offset-based pagination (simple, good for small datasets)
GET /api/v1/users?page=3&per_page=20

# Cursor-based pagination (better for large/real-time datasets)
GET /api/v1/users?after=usr_abc123&limit=20

# Keyset pagination (efficient for sorted data)
GET /api/v1/users?created_after=2026-01-01&limit=20
// Express.js pagination implementation
app.get('/api/v1/users', async (req, res) => {
  const page = Math.max(1, parseInt(req.query.page as string) || 1);
  const perPage = Math.min(100, Math.max(1,
    parseInt(req.query.per_page as string) || 20
  ));
  const offset = (page - 1) * perPage;

  const [users, total] = await Promise.all([
    db.users.findMany({ skip: offset, take: perPage }),
    db.users.count(),
  ]);

  const totalPages = Math.ceil(total / perPage);
  const baseUrl = '/api/v1/users';

  res.json({
    data: users,
    meta: { page, perPage, total, totalPages },
    links: {
      self: `${baseUrl}?page=${page}&per_page=${perPage}`,
      first: `${baseUrl}?page=1&per_page=${perPage}`,
      ...(page > 1 && {
        prev: `${baseUrl}?page=${page - 1}&per_page=${perPage}`
      }),
      ...(page < totalPages && {
        next: `${baseUrl}?page=${page + 1}&per_page=${perPage}`
      }),
      last: `${baseUrl}?page=${totalPages}&per_page=${perPage}`,
    },
  });
});

Filtering, Sorting, and Field Selection

# Filtering — use query parameters
GET /api/v1/users?role=admin&status=active
GET /api/v1/orders?created_after=2026-01-01&total_gte=100
GET /api/v1/products?category=electronics&price_lte=500

# Sorting — use sort parameter with +/- prefix
GET /api/v1/users?sort=-created_at          # Descending
GET /api/v1/users?sort=name                  # Ascending
GET /api/v1/users?sort=-created_at,+name     # Multiple

# Field selection — reduce response payload
GET /api/v1/users?fields=id,name,email
GET /api/v1/users/123?fields=name,avatar,posts.title

# Search — full-text search parameter
GET /api/v1/users?q=alice+johnson
GET /api/v1/products?search=wireless+keyboard

Versioning

API versioning protects existing clients from breaking changes. There are three common approaches.

# 1. URL path versioning (most common, recommended)
GET /api/v1/users
GET /api/v2/users

# 2. Header versioning
GET /api/users
Accept: application/vnd.myapi.v2+json

# 3. Query parameter versioning
GET /api/users?version=2
// Express.js versioned routes
import express from 'express';
import v1Router from './routes/v1';
import v2Router from './routes/v2';

const app = express();

// Mount versioned routes
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// v1/routes.ts
const router = express.Router();
router.get('/users', getUsersV1);
router.get('/users/:id', getUserV1);

// v2/routes.ts — same endpoints, new response format
const router = express.Router();
router.get('/users', getUsersV2);
router.get('/users/:id', getUserV2);

Authentication and Authorization

Use standard authentication mechanisms. JWT Bearer tokens are the most common choice for stateless APIs. For detailed coverage, see our API Authentication Guide and JWT Authentication Guide.

// Authentication middleware
import jwt from 'jsonwebtoken';

interface AuthRequest extends Request {
  user?: { id: string; role: string; email: string };
}

function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({
      error: {
        code: 'MISSING_TOKEN',
        message: 'Authorization header with Bearer token required',
      },
    });
  }

  const token = authHeader.slice(7);

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
    req.user = { id: decoded.sub, role: decoded.role, email: decoded.email };
    next();
  } catch (error) {
    return res.status(401).json({
      error: {
        code: 'INVALID_TOKEN',
        message: 'Token is invalid or expired',
      },
    });
  }
}

// Role-based authorization
function authorize(...roles: string[]) {
  return (req: AuthRequest, res: Response, next: NextFunction) => {
    if (!req.user || !roles.includes(req.user.role)) {
      return res.status(403).json({
        error: {
          code: 'FORBIDDEN',
          message: 'Insufficient permissions for this action',
        },
      });
    }
    next();
  };
}

// Apply to routes
app.get('/api/v1/users', authenticate, authorize('admin'), getUsers);
app.get('/api/v1/profile', authenticate, getProfile);

Rate Limiting

// Rate limiting with standard headers
import rateLimit from 'express-rate-limit';

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                  // 100 requests per window
  standardHeaders: true,     // RateLimit-* headers
  legacyHeaders: false,
  message: {
    error: {
      code: 'RATE_LIMITED',
      message: 'Too many requests. Please try again later.',
    },
  },
});

app.use('/api/', apiLimiter);

// Response headers:
// RateLimit-Limit: 100
// RateLimit-Remaining: 87
// RateLimit-Reset: 1708646400

HATEOAS: Hypermedia Links

HATEOAS (Hypermedia As The Engine Of Application State) makes your API self-discoverable by including links to related resources and available actions in responses.

// GET /api/v1/orders/ord_123
{
  "data": {
    "id": "ord_123",
    "status": "pending",
    "total": 99.99,
    "items": [
      { "productId": "prod_456", "quantity": 2, "price": 49.99 }
    ]
  },
  "links": {
    "self": "/api/v1/orders/ord_123",
    "customer": "/api/v1/users/usr_789",
    "items": "/api/v1/orders/ord_123/items",
    "cancel": "/api/v1/orders/ord_123/cancel",
    "pay": "/api/v1/orders/ord_123/pay"
  },
  "actions": {
    "cancel": {
      "method": "POST",
      "href": "/api/v1/orders/ord_123/cancel"
    },
    "pay": {
      "method": "POST",
      "href": "/api/v1/orders/ord_123/pay",
      "fields": [
        { "name": "paymentMethod", "type": "string", "required": true }
      ]
    }
  }
}

Caching

// Implement HTTP caching headers
app.get('/api/v1/products/:id', async (req, res) => {
  const product = await db.products.findById(req.params.id);

  // ETag for conditional requests
  const etag = generateETag(product);
  res.set('ETag', etag);

  // If client has current version, return 304
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }

  // Cache-Control for CDN and browser caching
  res.set('Cache-Control', 'public, max-age=300, s-maxage=600');

  // Last-Modified for time-based caching
  res.set('Last-Modified', product.updatedAt.toUTCString());

  res.json({ data: product });
});

// Vary header for content negotiation
app.get('/api/v1/articles', (req, res) => {
  res.set('Vary', 'Accept-Language, Accept');
  // ...
});

OpenAPI Documentation

# openapi.yaml — Document your API
openapi: 3.1.0
info:
  title: My REST API
  version: 1.0.0
  description: Production API for managing users and orders

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

paths:
  /users:
    get:
      summary: List all users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: per_page
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        '200':
          description: Paginated user list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

components:
  schemas:
    User:
      type: object
      required: [id, name, email]
      properties:
        id:
          type: string
          example: usr_abc123
        name:
          type: string
          example: Alice Johnson
        email:
          type: string
          format: email

REST API Design Checklist

CategoryPracticeStatus
URLsUse plural nouns for resourcesRequired
URLsUse kebab-case, lowercaseRequired
MethodsUse correct HTTP methodsRequired
Status CodesReturn appropriate status codesRequired
ResponsesConsistent response envelopeRequired
ErrorsStructured error responsesRequired
PaginationPaginate all list endpointsRequired
VersioningVersion your API from day oneRequired
AuthJWT or OAuth2 authenticationRequired
Rate LimitingProtect against abuseRequired
CachingETag and Cache-Control headersRecommended
HATEOASInclude hypermedia linksRecommended
FilteringQuery parameter filteringRecommended
DocsOpenAPI specificationRequired
CORSConfigure CORS headersRequired

Designing a great REST API is about consistency, predictability, and developer experience. Follow these conventions from the start, document everything with OpenAPI, and your API consumers will thank you. For more API resources, explore our CORS Errors Guide, HTTP Status Codes Guide, and JSON Formatter for testing API responses.

𝕏 Twitterin LinkedIn
Czy to było pomocne?

Bądź na bieżąco

Otrzymuj cotygodniowe porady i nowe narzędzia.

Bez spamu. Zrezygnuj kiedy chcesz.

Try These Related Tools

{ }JSON Formatter4xxHTTP Status Code ReferenceJWTJWT Decoder🔗URL Parser

Related Articles

REST API Najlepsze Praktyki: Kompletny Przewodnik na 2026

Poznaj najlepsze praktyki projektowania REST API: konwencje nazewnictwa, obsługa błędów, uwierzytelnianie i bezpieczeństwo.

Uwierzytelnianie API: OAuth 2.0 vs JWT vs API Key

Porównaj metody uwierzytelniania API: OAuth 2.0, tokeny JWT Bearer i klucze API.

Uwierzytelnianie JWT: Kompletny przewodnik implementacji

Zaimplementuj uwierzytelnianie JWT od zera. Struktura tokenow, tokeny dostepu i odswiezania, implementacja Node.js, zarzadzanie po stronie klienta, najlepsze praktyki bezpieczenstwa i middleware Next.js.

Jak naprawić błędy CORS: Kompletny przewodnik

Napraw błędy CORS krok po kroku.