API Design¶
RESTful API design principles and implementation in the Gold Standard Module.
Overview¶
The Gold Standard Module provides a fully-featured REST API following modern best practices.
flowchart LR
Client --> Auth[Authentication]
Auth --> Router
Router --> Controller
Controller --> Service
Service --> Repository
Repository --> Database
style Auth fill:#f9f,stroke:#333
style Controller fill:#bbf,stroke:#333
style Service fill:#bfb,stroke:#333 API Design Principles¶
| Principle | Implementation |
|---|---|
| RESTful | Resource-based URLs, HTTP methods |
| JSON:API | Consistent response format |
| Versioned | URL-based versioning (/api/v1/) |
| Authenticated | JWT or API key authentication |
| Documented | OpenAPI 3.0 specification |
URL Structure¶
Resource Naming¶
GET /api/v1/articles # List articles
GET /api/v1/articles/{id} # Get single article
POST /api/v1/articles # Create article
PUT /api/v1/articles/{id} # Update article
DELETE /api/v1/articles/{id} # Delete article
GET /api/v1/articles/{id}/comments # Article comments
POST /api/v1/articles/{id}/comments # Add comment
URL Conventions¶
- Use plural nouns:
/articlesnot/article - Use hyphens:
/api-keysnot/api_keys - Lowercase only
- No trailing slashes
- No file extensions
Request/Response Format¶
Request Headers¶
Content-Type: application/json
Accept: application/json
Authorization: Bearer {token}
X-API-Version: 2026-01-01
Success Response¶
{
"data": {
"type": "article",
"id": "01HX1234567890ABCDEFGHIJK",
"attributes": {
"title": "Article Title",
"content": "Article content...",
"status": "published",
"created_at": "2026-01-29T12:00:00Z"
},
"relationships": {
"author": {
"data": { "type": "user", "id": "01HX..." }
},
"category": {
"data": { "type": "category", "id": "01HX..." }
}
}
},
"meta": {
"version": "1.0.0"
}
}
Error Response¶
{
"errors": [
{
"status": "422",
"code": "validation_error",
"title": "Validation Failed",
"detail": "Title must be at least 5 characters",
"source": {
"pointer": "/data/attributes/title"
}
}
]
}
Collection Response¶
{
"data": [
{ "type": "article", "id": "01HX...", "attributes": {} },
{ "type": "article", "id": "01HX...", "attributes": {} }
],
"meta": {
"total": 150,
"page": 1,
"per_page": 20,
"total_pages": 8
},
"links": {
"self": "/api/v1/articles?page=1",
"first": "/api/v1/articles?page=1",
"prev": null,
"next": "/api/v1/articles?page=2",
"last": "/api/v1/articles?page=8"
}
}
HTTP Status Codes¶
| Code | Meaning | Usage |
|---|---|---|
| 200 | OK | Successful GET, PUT |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid JSON |
| 401 | Unauthorized | Missing/invalid auth |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource doesn't exist |
| 422 | Unprocessable | Validation failed |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Server Error | Unexpected error |
Authentication¶
JWT Authentication¶
POST /api/v1/auth/token
Content-Type: application/json
{
"username": "user@example.com",
"password": "password123"
}
Response:
{
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJl..."
}
}
API Key Authentication¶
Controller Implementation¶
<?php
declare(strict_types=1);
namespace Xoops\GoldStandard\Presentation\Api;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
#[Route('/api/v1/articles')]
final readonly class ArticleApiController
{
public function __construct(
private ArticleService $articleService,
private ArticleQueryService $queryService,
private ResponseFactory $responseFactory,
) {}
#[Route('', methods: ['GET'])]
public function index(ServerRequestInterface $request): ResponseInterface
{
$page = (int) ($request->getQueryParams()['page'] ?? 1);
$perPage = (int) ($request->getQueryParams()['per_page'] ?? 20);
$result = $this->queryService->findPublished($page, $perPage);
return $this->responseFactory->collection(
data: $result->items,
meta: [
'total' => $result->total,
'page' => $page,
'per_page' => $perPage,
],
);
}
#[Route('/{id}', methods: ['GET'])]
public function show(string $id): ResponseInterface
{
$article = $this->articleService->getArticle($id);
if ($article === null) {
return $this->responseFactory->notFound('Article not found');
}
return $this->responseFactory->item($article);
}
#[Route('', methods: ['POST'])]
#[RequireAuth]
public function store(ServerRequestInterface $request): ResponseInterface
{
$data = $request->getParsedBody();
$command = new CreateArticleCommand(
title: $data['title'] ?? '',
content: $data['content'] ?? '',
authorId: $request->getAttribute('user_id'),
);
try {
$article = $this->articleService->createArticle($command);
return $this->responseFactory->created($article);
} catch (ValidationException $e) {
return $this->responseFactory->validationError($e->errors());
}
}
#[Route('/{id}', methods: ['PUT'])]
#[RequireAuth]
#[RequirePermission('article_edit')]
public function update(string $id, ServerRequestInterface $request): ResponseInterface
{
$data = $request->getParsedBody();
$command = new UpdateArticleCommand(
id: $id,
title: $data['title'] ?? null,
content: $data['content'] ?? null,
);
try {
$article = $this->articleService->updateArticle($command);
return $this->responseFactory->item($article);
} catch (NotFoundException $e) {
return $this->responseFactory->notFound($e->getMessage());
}
}
#[Route('/{id}', methods: ['DELETE'])]
#[RequireAuth]
#[RequirePermission('article_delete')]
public function destroy(string $id): ResponseInterface
{
try {
$this->articleService->deleteArticle($id);
return $this->responseFactory->noContent();
} catch (NotFoundException $e) {
return $this->responseFactory->notFound($e->getMessage());
}
}
}
Query Parameters¶
Filtering¶
GET /api/v1/articles?filter[status]=published
GET /api/v1/articles?filter[category]=01HX...
GET /api/v1/articles?filter[author]=01HX...
Sorting¶
GET /api/v1/articles?sort=created_at
GET /api/v1/articles?sort=-created_at # Descending
GET /api/v1/articles?sort=title,-created_at # Multiple
Pagination¶
Including Relations¶
Sparse Fieldsets¶
Rate Limiting¶
When exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
{
"errors": [{
"status": "429",
"code": "rate_limit_exceeded",
"title": "Rate Limit Exceeded",
"detail": "You have exceeded 60 requests per minute"
}]
}
Versioning Strategy¶
flowchart TB
subgraph "Version Strategy"
V1["/api/v1/"] --> |"Stable"| S1[Current]
V2["/api/v2/"] --> |"Beta"| S2[Next Version]
V0["/api/"] --> |"Redirects"| V1
end Deprecation Headers¶
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Deprecation: true
Link: </api/v2/articles>; rel="successor-version"
OpenAPI Specification¶
openapi: 3.0.3
info:
title: Gold Standard API
version: 1.0.0
description: REST API for the Gold Standard Module
servers:
- url: https://example.com/api/v1
paths:
/articles:
get:
summary: List articles
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: per_page
in: query
schema:
type: integer
default: 20
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/ArticleCollection'
components:
schemas:
Article:
type: object
properties:
type:
type: string
example: article
id:
type: string
example: 01HX1234567890ABCDEFGHIJK
attributes:
type: object
properties:
title:
type: string
content:
type: string
status:
type: string
enum: [draft, published, archived]