Your API is used by other developers. They're going to curse you if it's hard to use. Make your API pleasant to work with.
Good developer experience means:
- Clear documentation: Obvious endpoints, clear examples
- Consistency: Predictable behavior
- Good errors: Errors that tell you what went wrong and how to fix it
- Tooling support: Postman collections, OpenAPI specs, SDKs
- Fast feedback: Clear request/response examples
- Versioning: Plan for API changes without breaking users
This chapter is about being a good API citizen. Your users should enjoy using your API.
Environment Configuration
Document all environment variables:
# .env.example
# Application
APP_NAME="Domain API"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://api.example.com
# Database
DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=api_domain
DB_USERNAME=root
DB_PASSWORD=password
# Cache
CACHE_DRIVER=redis
REDIS_HOST=localhost
REDIS_PORT=6379
# Queue
QUEUE_CONNECTION=redis
# External Services
GODADDY_API_KEY=your-key
GODADDY_API_SECRET=your-secret
GODADDY_ENVIRONMENT=production
# Logging
LOG_CHANNEL=stack
LOG_LEVEL=debug
# Localization
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
Type Hints & Documentation
Use PHPDoc for clarity:
/**
* Create a DNS record for a domain.
*
* @param string $domain FQDN (e.g., 'example.com')
* @param string $ip IP address (IPv4 or IPv6)
* @param string $type DNS record type (A, AAAA, CNAME, etc.)
* @param string $name Record name (@ for root, subdomain for others)
* @param int $ttl Time to live in seconds
*
* @return void
*
* @throws DomainNotFound
* @throws InvalidDnsRecord
* @throws DomainProviderException
*/
public function deleteLicense(
string $domain,
string $ip,
string $type = 'A',
string $name = '@',
int $ttl = 3600,
): void
API Documentation
Generate documentation from code:
# Using Laravel API docs generator
composer require --dev scribe-php/scribe
php artisan scribe:init
php artisan scribe:generate
Or write Markdown documentation:
# API Documentation
## Authentication
All endpoints require Bearer token authentication:
```http
Authorization: Bearer your-token-here
```
## Rate Limiting
- Limit: 100 requests per minute per token
- Headers: `X-RateLimit-*` included in every response
## Domain Endpoints
## Get Licenses
```http
GET /api/licenses
Authorization: Bearer {token}
```
Response:
```json
{
"items": [
{
"key": "lic_abc123xyz",
"name": "My License",
"domain": "example.com",
"status": "active",
"expiresAt": "2025-12-31T23:59:59Z",
"createdAt": "2024-01-01T00:00:00Z"
}
]
}
```
Code Examples
Provide worked examples:
// examples/check-domain-availability.php
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://api.example.com',
'http_errors' => false,
]);
$response = $client->post('/api/licenses', [
'headers' => [
'Authorization' => 'Bearer your-token',
],
'json' => [
'name' => 'My License',
'domain' => 'example.com',
],
]);
$data = json_decode($response->getBody(), true);
if ($response->getStatusCode() === 201) {
echo "License created: " . $data['data']['key'];
echo "Status: " . $data['data']['status'];
} else {
echo "Error: " . $data['error'];
}
Helpful Error Messages
Provide errors that help developers fix issues:
// ❌ Bad error message
"error": "Invalid input"
// ✅ Good error message
{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"fields": {
"email": ["Email must be valid"],
"phone": ["Phone must be in E.164 format (+country-code-number)"]
}
}
Backwards Compatibility
Maintain compatibility when evolving your API:
// Version your API
Route::prefix('/api/v1')->group(function () {
// V1 endpoints
Route::get('/domains', [LicenseControllerV1::class, 'index']);
});
Route::prefix('/api/v2')->group(function () {
// V2 endpoints (better response format)
Route::get('/domains', [LicenseControllerV2::class, 'index']);
});
// Deprecation headers
class DeprecationMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
if ($request->is('api/v1/*')) {
$response->header('Deprecation', 'true');
$response->header('Sunset', now()->addYear()->toRfc2822String());
$response->header('Link', '<https://docs.example.com/v2>; rel="latest"');
}
return $response;
}
}