Advanced PHP API Development
Modern PHP APIs power mobile apps, SPAs, and microservices. This guide covers advanced patterns beyond basic CRUD — authentication, versioning, validation, and documentation.
API Architecture Overview
Client → Nginx → PHP-FPM → Router → Controller → Service → Repository → DB
↓
Middleware (auth, rate limit, CORS)
Separate concerns: controllers handle HTTP, services contain business logic, repositories handle data access.
RESTful Design Principles
| Method | Path | Action |
|---|---|---|
| GET | /api/v1/users |
List users |
| GET | /api/v1/users/42 |
Get user 42 |
| POST | /api/v1/users |
Create user |
| PUT | /api/v1/users/42 |
Replace user 42 |
| PATCH | /api/v1/users/42 |
Partial update |
| DELETE | /api/v1/users/42 |
Delete user 42 |
Use proper HTTP status codes:
// 200 OK, 201 Created, 204 No Content
// 400 Bad Request, 401 Unauthorized, 403 Forbidden
// 404 Not Found, 422 Unprocessable Entity, 429 Too Many Requests
// 500 Internal Server Error
Slim Framework Example
<?php
declare(strict_types=1);
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
require __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addBodyParsingMiddleware();
$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, true, true);
// CORS middleware
$app->add(function (Request $request, $handler) {
$response = $handler->handle($request);
return $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
});
$app->get('/api/v1/users/{id}', function (Request $request, Response $response, array $args) {
$user = getUserById((int) $args['id']);
if ($user === null) {
$payload = json_encode(['error' => 'User not found']);
$response->getBody()->write($payload);
return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
}
$response->getBody()->write(json_encode(['data' => $user]));
return $response->withHeader('Content-Type', 'application/json');
});
$app->run();
JWT Authentication
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
function generateToken(int $userId, string $secret): string
{
$payload = [
'sub' => $userId,
'iat' => time(),
'exp' => time() + 3600,
];
return JWT::encode($payload, $secret, 'HS256');
}
function authMiddleware(Request $request, $handler)
{
$header = $request->getHeaderLine('Authorization');
if (!str_starts_with($header, 'Bearer ')) {
return new JsonResponse(['error' => 'Unauthorized'], 401);
}
$token = substr($header, 7);
try {
$decoded = JWT::decode($token, new Key(getenv('JWT_SECRET'), 'HS256'));
$request = $request->withAttribute('userId', $decoded->sub);
return $handler->handle($request);
} catch (Exception $e) {
return new JsonResponse(['error' => 'Invalid token'], 401);
}
}
Input Validation
Never trust client input:
function validateCreateUser(array $data): array
{
$errors = [];
if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Valid email is required';
}
if (empty($data['name']) || strlen($data['name']) > 100) {
$errors['name'] = 'Name is required (max 100 chars)';
}
if (!empty($errors)) {
throw new ValidationException($errors);
}
return [
'email' => strtolower(trim($data['email'])),
'name' => trim($data['name']),
];
}
Laravel Form Requests and Symfony Validator provide declarative validation.
API Versioning
URL versioning (most common):
/api/v1/users
/api/v2/users
Header versioning:
Accept: application/vnd.myapp.v2+json
Maintain v1 while building v2 — deprecate with sunset headers:
$response = $response->withHeader('Deprecation', 'true')
->withHeader('Sunset', 'Sat, 01 Jan 2026 00:00:00 GMT');
Rate Limiting
function rateLimitMiddleware(Redis $redis, int $maxRequests = 100, int $windowSeconds = 60)
{
return function (Request $request, $handler) use ($redis, $maxRequests, $windowSeconds) {
$ip = $request->getServerParams()['REMOTE_ADDR'];
$key = "rate:{$ip}:" . floor(time() / $windowSeconds);
$count = $redis->incr($key);
if ($count === 1) {
$redis->expire($key, $windowSeconds);
}
if ($count > $maxRequests) {
return new JsonResponse(['error' => 'Rate limit exceeded'], 429);
}
$response = $handler->handle($request);
return $response->withHeader('X-RateLimit-Remaining', (string) ($maxRequests - $count));
};
}
Consistent Response Format
function jsonResponse(mixed $data, int $status = 200): Response
{
$body = json_encode([
'data' => $data,
'meta' => [
'timestamp' => date('c'),
],
], JSON_THROW_ON_ERROR);
$response = new Response();
$response->getBody()->write($body);
return $response->withStatus($status)->withHeader('Content-Type', 'application/json');
}
function jsonError(string $message, int $status, array $details = []): Response
{
$body = json_encode([
'error' => [
'message' => $message,
'details' => $details,
],
], JSON_THROW_ON_ERROR);
$response = new Response();
$response->getBody()->write($body);
return $response->withStatus($status)->withHeader('Content-Type', 'application/json');
}
Pagination
$app->get('/api/v1/products', function (Request $request, Response $response) {
$page = max(1, (int) ($request->getQueryParams()['page'] ?? 1));
$perPage = min(100, max(1, (int) ($request->getQueryParams()['per_page'] ?? 20)));
$offset = ($page - 1) * $perPage;
$products = getProducts($offset, $perPage);
$total = countProducts();
$body = json_encode([
'data' => $products,
'meta' => [
'page' => $page,
'per_page' => $perPage,
'total' => $total,
'total_pages' => (int) ceil($total / $perPage),
],
]);
$response->getBody()->write($body);
return $response->withHeader('Content-Type', 'application/json');
});
OpenAPI Documentation
Document your API with OpenAPI 3.0 — tools like Swagger UI auto-generate interactive docs:
openapi: 3.0.0
info:
title: My API
version: 1.0.0
paths:
/api/v1/users/{id}:
get:
summary: Get user by ID
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: User found
'404':
description: User not found
Laravel: darkaonline/l5-swagger. Symfony: nelmio/api-doc-bundle.
Security Checklist
- HTTPS only — redirect HTTP, HSTS header
- JWT with short expiry + refresh tokens
- Input validation on every endpoint
- SQL injection prevention — prepared statements / ORM
- Rate limiting per IP and per user
- CORS restricted to known origins in production
- No sensitive data in error responses
- API keys rotated regularly
Production PHP APIs rival any language’s capabilities — Laravel and Symfony provide enterprise-grade tooling when you need frameworks at scale.