RESTful API Design Best Practices

লেসন ২৯ / ৫০

REST Principles ⭐

  1. Client-Server
  2. Stateless (no session)
  3. Cacheable
  4. Uniform interface
  5. Layered system

Resource URL Naming

✅ DO:
GET    /api/products              # collection
GET    /api/products/42           # specific
POST   /api/products              # create
PUT    /api/products/42           # full update
PATCH  /api/products/42           # partial update
DELETE /api/products/42           # delete
GET    /api/products/42/reviews   # nested

❌ AVOID:
GET /api/getProducts
GET /api/product/create
POST /api/products/delete/42
GET /api/api/v1/users/getUserById?id=5

Naming Rules

  • Plural nouns: products not product
  • kebab-case: /order-items not /orderItems
  • Lowercase: /users not /Users
  • No verbs: action HTTP method-এ থাকে
  • Hierarchy: /users/{id}/orders/{orderId}

Query Parameters

GET /api/products?
    search=laptop&            # search
    category=5&                # filter
    minPrice=100&maxPrice=1000 # range
    sortBy=price&order=desc&   # sort
    page=2&pageSize=20&        # pagination
    fields=id,name,price       # field selection (sparse fieldsets)

Status Codes ⭐

Codeকখন
200 OKসফল GET, PUT, PATCH
201 CreatedPOST সফল
204 No Contentসফল DELETE
400 Bad RequestInvalid input
401 UnauthorizedAuthentication missing/invalid
403 ForbiddenAuthenticated but not allowed
404 Not FoundResource doesn't exist
409 ConflictDuplicate, version conflict
422 UnprocessableValidation failed
429 Too ManyRate limited
500 Server ErrorBug

Response Format Standard

// Success — single
{
  "data": { "id": 1, "name": "Pen" }
}

// Success — list with pagination
{
  "data": [...],
  "meta": {
    "total": 150,
    "page": 2,
    "pageSize": 20,
    "totalPages": 8
  },
  "links": {
    "self": "/api/products?page=2",
    "next": "/api/products?page=3",
    "prev": "/api/products?page=1"
  }
}

// Error
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Invalid input",
    "details": [
      { "field": "email", "message": "Invalid format" }
    ],
    "traceId": "abc-123"
  }
}

Versioning Strategies

// 1. URL path ⭐
GET /api/v1/products
GET /api/v2/products

// 2. Query string
GET /api/products?api-version=1.0

// 3. Header
GET /api/products
X-API-Version: 2.0

// 4. Media type
Accept: application/vnd.myapi.v2+json

HATEOAS

{
  "id": 42, "status": "pending",
  "_links": {
    "self":   { "href": "/orders/42" },
    "cancel": { "href": "/orders/42/cancel", "method": "POST" }
  }
}

ASP.NET Core Implementation

[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
public class ProductsController : ControllerBase {

    [HttpGet]
    public async Task<ActionResult<PagedResult<ProductDto>>> GetAll(
        [FromQuery] string? search,
        [FromQuery] int? categoryId,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20)
    {
        var query = _db.Products.AsQueryable();
        if (!string.IsNullOrEmpty(search))
            query = query.Where(p => p.Name.Contains(search));
        if (categoryId.HasValue)
            query = query.Where(p => p.CategoryId == categoryId);

        var total = await query.CountAsync();
        var items = await query
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(p => new ProductDto { ... })
            .ToListAsync();

        return Ok(new PagedResult<ProductDto> {
            Data = items,
            Meta = new { Total = total, Page = page, PageSize = pageSize }
        });
    }
}