NOTE: These guidelines are heavily opinionated and may not be suitable for all use cases. Please adapt them to your specific requirements.
400 Bad Request
: The request could not be understood by the server due to malformed syntax (often invalid query parameters or request body). Return a RFC 7807 problem detail object with a clear error message.401 Unauthorized
: The client must authenticate itself to get the requested response. This is usually returned when theAuthorization
header is missing or invalid.500 Internal Server Error
: Return a RFC 7807 problem detail object. It is recommended to extend the problem detail object with atraceId
andrequestId
field in this case to help with debugging.
- Use
GET
for fetching resources. GET
requests should be safe and idempotent.- Use appropriate cache headers to optimize performance.
200 OK
: Successful response.404 Not Found
: Resource not found or user does not have access.403 Forbidden
: Authenticated user does not have access to this resource. It is recommended to use404
instead of403
forGET
requests to avoid exposing sensitive information.
- Use
POST
for creating resources. POST
requests should not be idempotent.POST
requests should never be cached.
201 Created
: Resource created successfully. Include aLocation
header with the URL of the new resource.200 OK
or204 No Content
: Resource created successfully but the resource cannot be identified by a URI. Use thePrefer
header to determine the response format.403 Forbidden
: Authenticated user does not have permission to create the resource.
- Use
PUT
for updating resources. The API may choose to create the resource if it does not exist. PUT
requests should be idempotent.PUT
requests should not be cached.
200 OK
or204 No Content
: Resource updated successfully. Use thePrefer
header to determine the response format.201 Created
: New resource created successfully.
- Use
DELETE
for deleting resources. DELETE
requests should be idempotent.DELETE
requests should not be cached.- Soft delete: Consider soft delete (marking the resource as deleted) instead of hard delete (removing the resource from the database).
200 OK
or204 No Content
: Resource deleted successfully. Use thePrefer
header to determine the response format.202 Accepted
: The request has been accepted for processing, but the processing has not been completed.404 Not Found
: Resource not found or user does not have access.
- Use
PATCH
for partial updates to resources. PATCH
requests should be idempotent.PATCH
requests should not be cached.
[
{ "op": "test", "path": "/a/b/c", "value": "foo" },
{ "op": "remove", "path": "/a/b/c" },
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
{ "op": "replace", "path": "/a/b/c", "value": 42 },
{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]
200 OK
or204 No Content
: Resource updated successfully. Use thePrefer
header to determine the response format.404 Not Found
: Resource not found or user does not have access.
page
- 1-based page numberpageSize
- number of items per page
Provide default values for page
and pageSize
if not specified by the client. The default values should be page=1
and pageSize=10
(may vary based on resource).
Simple filtering is usable for most filtering requirements.
- Flexible parameters: Use query parameters that represent filterable fields.
- Example of simple filtering:
GET /resources?status=active&category=books
- Example of range-based filtering:
GET /resources?minPrice=10&maxPrice=100
- Example of exclusion filtering:
GET /resources?excludeStatus=inactive
- Example of simple filtering:
- Multiple values: Use comma-separated values for filtering on multiple values of the same field.
- Example:
GET /resources?category=books,electronics
- Example:
- Date filtering: Use ISO 8601 date format for date filtering.
- Example:
GET /resources?createdAfter=2023-01-01T00:00:00Z&createdBefore=2023-12-31T23:59:59Z
- Example:
- Boolean filtering: Use
true
orfalse
for boolean filtering.- Example:
GET /resources?active=true
- Example:
- Filtering on nested fields: Use dot notation for filtering on nested fields.
- Example:
GET /resources?author.name=John
- Example:
For complex filtering requirements, consider using a filter
parameter with a query language like OData.
- Syntax: Use a
filter
parameter with a query language expression.- Example:
GET /resources?filter=status eq 'active' and price lt 100
- Example:
Read more on Microsoft's API guidelines.
- Syntax: Use a
sort
parameter, with fields and optional direction- Example:
GET /resources?sort=-price,name
(sort by price descending and name ascending)
- Example:
The default behavior should depend on the resource and should be documented.
{
"data": [],
"metadata": {
"page": 2,
"pageSize": 10,
"totalCount": 100,
"totalPages": 10
},
"links": {
"first": "/resources?page=1&pageSize=10",
"prev": "/resources?page=1&pageSize=10",
"self": "/resources?page=2&pageSize=10",
"next": "/resources?page=3&pageSize=10",
"last": "/resources?page=10&pageSize=10"
}
}
data
: Array of resourcesmetadata
: Pagination metadatapage
: Current page numberpageSize
: Number of items per pagetotalCount
: Total number of itemstotalPages
: Total number of pages
links
: Links to navigate through the paginated results (optional)first
: Link to the first pageprev
: Link to the previous page (if available)self
: Link to the current pagenext
: Link to the next page (if available)last
: Link to the last page
public sealed class PaginationResponse<T>(IEnumerable<T> data, PaginationMetadata metadata, PaginationLinks? links = null)
{
[JsonPropertyName("data")]
public IEnumerable<T> Data { get; } = data;
[JsonPropertyName("metadata")]
public PaginationMetadata Metadata { get; } = metadata;
[JsonPropertyName("links")]
public PaginationLinks? Links { get; } = links;
}
public sealed class PaginationMetadata(int page, int pageSize, int totalCount)
{
[JsonPropertyName("page")]
public int Page { get; } = page;
[JsonPropertyName("pageSize")]
public int PageSize { get; } = pageSize;
[JsonPropertyName("totalCount")]
public int TotalCount { get; } = totalCount;
[JsonPropertyName("totalPages")]
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
}
public sealed class PaginationLinks(string first, string? previous, string self, string? next, string last)
{
[JsonPropertyName("first")]
public string First { get; } = first;
[JsonPropertyName("prev")]
public string? Previous { get; } = previous;
[JsonPropertyName("self")]
public string Self { get; } = self;
[JsonPropertyName("next")]
public string? Next { get; } = next;
[JsonPropertyName("last")]
public string Last { get; } = last;
}
Prefer
header: Use thePrefer
header to request the server to return a minimal representation of the resource. This is not applicable forGET
requests.- Example:
Prefer: return=representation
- Example:
Prefer: return=minimal
- Example:
X-Total-Count
: Total number of itemsLink
: A standard header for pagination links, following the format specified in RFC 8288.
- Validation errors: If the client provides invalid parameters (e.g.,
page
< 1 or unsupported filter fields), return a400 Bad Request
with a detailed error message. - Range errors: If
page
exceeds thetotalPages
, respond with a404 Not Found
or a clear400 Bad Request
.
- Cache control: Use appropriate cache headers to optimize API performance.
- Limit maximum
pageSize
: To prevent performance degradation, set a maximum value forpageSize
(e.g.,100
items). - Cursor-based pagination: Consider using cursor-based pagination for large datasets to improve performance.