Every PHP developer builds APIs eventually. The difference between an API that works in development and one that survives production traffic comes down to decisions about structure, error handling, authentication, and versioning. Most of these decisions are not hard—they just need to be made deliberately rather than by accident.
The foundation: consistent response structure
Before writing any endpoint, decide on a response envelope. Every response from your API should follow the same shape, regardless of whether it is a success or error.
1 | // Success response |
This consistency means API consumers can always check for data on success and error on failure without guessing the response shape per endpoint.
Laravel API Resources
1 | class ArticleResource extends JsonResource |
whenLoaded prevents N+1 queries by only including relationships that were eagerly loaded. This is a detail that matters at scale.
Routing and URL design
Resource-oriented routes
1 | // Good: resource-oriented |
Nested resources
1 | Route::apiResource('articles.comments', CommentController::class) |
Shallow nesting avoids deeply nested URLs like /api/articles/5/comments/12/replies/3. Once a resource has its own ID, it does not need the parent in the URL.
Validation
Validate every incoming request at the controller boundary. Never trust client data.
1 | class StoreArticleRequest extends FormRequest |
Custom validation messages for API consumers
1 | public function messages(): array |
Authentication
Token-based authentication with Laravel Sanctum
For most PHP APIs, Sanctum provides the right balance of simplicity and security:
1 | // Issue a token |
1 | // Protect routes |
Rate limiting
1 | // bootstrap/app.php or RouteServiceProvider |
Rate limit authentication endpoints aggressively (5/minute) and general API endpoints moderately (60/minute). Return proper 429 Too Many Requests responses with Retry-After headers.
Error handling
Centralized exception handling
1 | // bootstrap/app.php |
Status code reference
| Code | When to use |
|---|---|
| 200 | Successful GET, PUT, PATCH |
| 201 | Successful POST (resource created) |
| 204 | Successful DELETE (no content) |
| 400 | Malformed request syntax |
| 401 | Missing or invalid authentication |
| 403 | Authenticated but not authorized |
| 404 | Resource not found |
| 409 | Conflict (duplicate, state mismatch) |
| 422 | Validation failure |
| 429 | Rate limit exceeded |
| 500 | Server error (log it, alert on it) |
API versioning
URI versioning (most common)
1 | Route::prefix('v1')->group(function () { |
Header versioning (cleaner but less discoverable)
1 | Route::middleware('api.version:2')->group(function () { |
URI versioning is easier for API consumers to understand and test. Use it unless you have a specific reason for header versioning.
Common mistakes
Not paginating list endpoints: Returning 10,000 records in a single response will eventually crash something. Always paginate.
Exposing internal IDs: Use UUIDs or hashed IDs in public-facing APIs. Auto-increment integers leak information about your data volume.
Ignoring CORS: If your API serves a frontend on a different domain, configure CORS properly instead of using Access-Control-Allow-Origin: * in production.
Missing content type headers: Always set Content-Type: application/json on responses. Some HTTP clients behave differently without it.
Inconsistent date formats: Pick ISO 8601 (2026-03-15T18:24:00Z) and use it everywhere. Never mix Unix timestamps with formatted date strings.
Production tradeoffs
Framework API vs. microframework: Laravel’s API scaffolding saves time but adds overhead. For tiny microservices, Slim or Mezzio might be more appropriate. For anything with authentication, authorization, or database access, the framework overhead is worth it.
REST vs. GraphQL: REST is simpler to cache, easier to debug, and better supported by CDNs. Choose GraphQL only if your clients genuinely need flexible queries across related resources. See the related guide on GraphQL for PHP developers for a deeper comparison.
JSON:API spec vs. custom envelope: JSON:API provides a standard response format but adds complexity. Custom envelopes are simpler when you control both the API and its consumers.
FAQ
Should I use PUT or PATCH for updates?
Use PUT for full replacement and PATCH for partial updates. If you only support one, PATCH is more practical since clients rarely send every field.
How do I handle file uploads in a REST API?
Use multipart/form-data for the upload endpoint. Return the file’s URL or ID in the JSON response. Do not try to base64-encode files in JSON—it triples the payload size.
What about HATEOAS?
In theory, API responses should include links to related actions. In practice, very few PHP APIs implement full HATEOAS. Include pagination links and leave it at that unless your API is genuinely consumed by automated agents.
Next steps
Start with a single resource endpoint. Get the response structure, validation, and error handling right on that one endpoint before building out the rest. The patterns you establish early become the template for everything that follows.
For database interaction patterns behind your API, the PDO tutorial covers the fundamentals. The Laravel 13 upgrade guide covers framework-level improvements that affect API development. For understanding PHP’s new language features that improve API code, see the PHP 8.5/8.6 guide.