In 2013 most PHP developers (us included) were building APIs as an afterthought - basically a page that outputs JSON instead of HTML. We made every mistake in the book on our first mobile project. This is the post I wish existed before we started.
Mistake #1: Using GET for Everything
Our original "API":
GET /api.php?action=get_users
GET /api.php?action=create_user&name=John&email=john@example.com
GET /api.php?action=delete_user&id=5
This is not an API. This is a script with query parameters. The problem isn't style - it's that GET requests are logged in server access logs (including sensitive parameters), cached by proxies, and bookmarkable by browsers. Passing a password or token in a GET URL is a security hole.
What REST actually means: Use HTTP verbs semantically.
GET /users → list users (safe, idempotent, cacheable)
POST /users → create user (not safe, not idempotent)
GET /users/42 → get specific user
PUT /users/42 → replace user (idempotent)
PATCH /users/42 → partial update
DELETE /users/42 → delete (idempotent)
Mistake #2: Inconsistent Response Envelopes
Our responses looked different depending on who wrote the endpoint:
// Endpoint A
{ "data": [...], "count": 5 }
// Endpoint B
{ "users": [...], "total": 5 }
// Endpoint C (error)
{ "error": 1, "msg": "Not found" }
// Endpoint D (error)
{ "success": false, "message": "User not found", "code": 404 }
The iOS developer on our team spent two days writing different parsers for different endpoints. Fix: define one envelope and use it everywhere.
// Success
{
"success": true,
"data": { ... },
"meta": { "total": 100, "page": 1, "per_page": 20 }
}
// Error
{
"success": false,
"error": {
"code": "USER_NOT_FOUND",
"message": "No user with id 42"
}
}
Mistake #3: Wrong HTTP Status Codes
// What we were doing
http_response_code(200); // Always 200
echo json_encode(['error' => 'User not found']);
The mobile client has to parse the body to know if the request succeeded. Every response is 200 OK even when the server is on fire.
// What HTTP status codes are for:
// 200 OK - success
// 201 Created - resource created (return it in body)
// 204 No Content - success, no body (DELETE)
// 400 Bad Request - client sent invalid data
// 401 Unauthorized - not authenticated
// 403 Forbidden - authenticated but not allowed
// 404 Not Found - resource doesn't exist
// 422 Unprocessable Entity - validation failed
// 500 Internal Server Error - our bug
function jsonResponse($data, $status = 200) {
http_response_code($status);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
// Usage
if (!$user) {
jsonResponse(['error' => ['code' => 'USER_NOT_FOUND']], 404);
}
Mistake #4: No Versioning
We deployed API v1. Three months later the iOS app needed the user response to include avatar_url. We added it. The Android developer had hardcoded the response structure. It broke Android.
Add versioning from day one:
/api/v1/users
/api/v2/users ← new format, v1 still works
In PHP:
// Router dispatches based on version
$version = $pathParts[1]; // 'v1' or 'v2'
$resource = $pathParts[2]; // 'users'
switch ($version) {
case 'v1':
require "controllers/v1/{$resource}Controller.php";
break;
case 'v2':
require "controllers/v2/{$resource}Controller.php";
break;
default:
jsonResponse(['error' => 'Unknown API version'], 400);
}
Authentication: From Sessions to Tokens
Web apps use sessions. Mobile apps can't - sessions are tied to cookies and don't survive app restarts properly.
We moved to stateless token authentication:
// On login: generate token, store in DB
function generateToken($userId) {
$token = bin2hex(random_bytes(32)); // 64 char hex string
$expiry = date('Y-m-d H:i:s', strtotime('+30 days'));
$db->query(
"INSERT INTO auth_tokens (user_id, token, expires_at) VALUES (?, ?, ?)",
[$userId, hash('sha256', $token), $expiry]
);
return $token; // Return raw token to client once
}
// On each API request: verify token from Authorization header
function authenticate() {
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!preg_match('/^Bearer (.+)$/', $authHeader, $matches)) {
jsonResponse(['error' => 'No token provided'], 401);
}
$token = $matches[1];
$row = $db->queryOne(
"SELECT user_id FROM auth_tokens
WHERE token = ? AND expires_at > NOW()",
[hash('sha256', $token)]
);
if (!$row) {
jsonResponse(['error' => 'Invalid or expired token'], 401);
}
return $row['user_id'];
}
This is a simplified version of what JWTs formalized later. We were reinventing a wheel, but at least it was a round wheel.
What Still Holds Up
These lessons from 2013 are still valid in 2024:
- Use HTTP verbs correctly - GET doesn't mutate state
- Consistent error format - clients shouldn't special-case every endpoint
- Correct status codes - let HTTP do what it was designed for
- Version your API - you will break clients if you don't
- Stateless auth - tokens, not sessions, for mobile/API clients
The tools changed (Slim → Express → FastAPI → tRPC), but the principles are the same.
Aunimeda builds production-grade backend systems - APIs, microservices, real-time applications, and system integrations.
Contact us for backend engineering services. See also: Custom Software Development, Web Development