This document outlines the guidelines for designing REST APIs within the project. It draws inspiration from Microsoft's API Design Best Practices and Google's AIPs, aiming for APIs that are intuitive, consistent, and easy to use for both humans and machines.
APIs should be organized around resources, which are the fundamental concepts of the business domain (e.g., Users, Orders, Tenants).
- Nouns, not Verbs: URLs must refer to resources (nouns), not actions (verbs).
- ✅
GET /users/123 - ❌
GET /getUser?id=123
- ✅
- Plural Nouns: Use plural nouns for resource collections.
- ✅
/users,/tenants,/documents - ❌
/user,/tenant,/document
- ✅
- Hierarchy: Use path segments to represent hierarchical relationships.
- ✅
/tenants/{tenantId}/users/{userId}
- ✅
- Kebab-case: Use lowercase letters and hyphens for URL path segments to ensure readability and consistency across systems.
- ✅
/api/v1/user-profiles - ❌
/api/v1/UserProfilesor/api/v1/user_profiles
- ✅
- Resources should have a unique identifier.
- The "name" of a resource is its full path (e.g.,
tenants/123/users/456). - Client-facing IDs should be URL-safe strings (e.g., UUIDs or alphanumeric IDs).
Use standard HTTP methods to represent actions performed on resources.
| Method | Action | Description | Idempotent | Body |
|---|---|---|---|---|
| GET | Retrieve | Retrieves a representation of the resource(s). Should not modify the server state. | Yes | No |
| POST | Create | Creates a new resource in a collection. The server assigns the ID. | No | Yes |
| PUT | Replace | Replaces the resource at the specified URI entirely. If it doesn't exist, it can be created (if client knows ID). | Yes | Yes |
| PATCH | Update | Partially updates the resource. Only fields present in the payload are updated. | Yes/No* | Yes |
| DELETE | Delete | Removes the resource. | Yes | No |
*PATCH should ideally be idempotent, but technically usually isn't in JSON-Patch unless carefully designed. Merge-PATCH (RFC 7396) is often preferred for simplicity.
We follow the standard method patterns defined in Google AIP-131 through AIP-135.
- Retrieves a list of resources.
- Must support pagination for large collections.
- Should support filtering and sorting.
- Returns a JSON object containing a list field (e.g.,
valuesoritems).
- Retrieves a single resource.
- Returns 404 Not Found if the resource does not exist.
- Creates a new resource.
- Returns
201 Created. - Returns a
Locationheader containing the URL of the newly created resource. - Response body contains the created resource.
- Updates specific fields of an existing resource.
- Prefer
PATCHoverPUTfor updates to avoid accidental data loss (overwriting fields not sent). - Use standard JSON Merge Patch (RFC 7396) semantics where possible: null deletes a field, value updates it.
- Deletes a resource.
- Returns
204 No Contenton success with no body. - Returns
200 OKif the deleted resource is returned in the body (rare). - Returns
404 Not Foundif the resource is already deleted (or204if you want to be idempotent and "ensure it is gone"). Recommendation: 404 for clarity in development, but idempotent 204 is also acceptable in production systems.
For actions that don't fit CRUD (e.g., "Undelete", "Checkout", "Publish"):
- Use the pattern:
POST /resource/{id}/action(orPOST /resource/{id}:actionper AIP, but/actionis friendlier for some web servers). - Recommendation:
POST /documents/123/publish
- kebab-case:
/system-settings,/users - Lowercase: Always.
- camelCase: The backend (C#) uses
PascalCasefor properties, but the serialized JSON must becamelCase. This is configured globally inProgram.cs.- ✅
firstName,createdAt - ❌
FirstName,created_at
- ✅
- Don't return all records. Use
limit(orpageSize) andoffset(orpage) or cursor-based pagination. - AIP Style (Preferred):
- Request:
pageSize(int),pageToken(string). - Response:
nextPageToken(string).
- Request:
- Use a query parameter
orderBy(orsort). - Format:
field(ascending) orfield desc(descending). - Example:
GET /users?orderBy=lastName,firstName desc
- Simple filtering:
GET /users?role=admin&active=true - Complex filtering: Use a
filterquery parameter with a structured syntax if needed, but prefer specific parameters for common filters.
- URI Versioning: Include the version number in the URL.
- Format:
/api/v{major}/... - Example:
/api/v1/users - Breaking changes require a new major version.
- Non-breaking changes (adding fields) do not require a new version.
Return standard HTTP status codes and a consistent error response body.
- 200 OK: Request succeeded.
- 201 Created: Resource created successfully.
- 204 No Content: Request succeeded, no body returned (DELETE, generic actions).
- 400 Bad Request: Invalid input (validation error, malformed JSON).
- 401 Unauthorized: Authentication required/failed.
- 403 Forbidden: Authenticated, but permissions denied.
- 404 Not Found: Resource does not exist.
- 409 Conflict: Resource state conflict (e.g., duplicate unique field).
- 500 Internal Server Error: Server crashed.
Return a JSON object with details.
{
"error": {
"code": "InvalidParameter",
"message": "The 'email' field must be a valid email address.",
"target": "email"
}
}- OpenAPI (Swagger): All APIs must be documented using OpenAPI specifications.
- Descriptions: Every endpoint and parameter must have a clear human-readable description.
- Examples: Provide example requests and responses in the documentation.
- Controllers: Use
[Route("api/v1/[controller]")]but ensure the controller name results in a proper plural noun, or explicitly set the route[Route("api/v1/users")]. - DTOs: Always use Data Transfer Objects (DTOs) for request/response bodies. Never expose Entity Framework entities directly.
- Validation: Use
FluentValidationto validate DTOs before processing.