AboutBlogContact
Backend EngineeringJuly 22, 2013 5 min read 149Updated: June 22, 2026

Building a Proper REST API in PHP: Lessons From Our First Mobile Backend

AunimedaAunimeda
📋 Table of Contents

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:

  1. Use HTTP verbs correctly - GET doesn't mutate state
  2. Consistent error format - clients shouldn't special-case every endpoint
  3. Correct status codes - let HTTP do what it was designed for
  4. Version your API - you will break clients if you don't
  5. 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

Read Also

Node.js + TypeScript: Building a Production REST API from Scratch in 2026aunimeda
Backend Engineering

Node.js + TypeScript: Building a Production REST API from Scratch in 2026

A complete guide to building a production-ready REST API with Node.js and TypeScript - authentication, validation, error handling, rate limiting, logging, and deployment. No shortcuts.

GraphQL vs REST API in 2026: A Practical Guide to Choosing the Right Approachaunimeda
Backend Engineering

GraphQL vs REST API in 2026: A Practical Guide to Choosing the Right Approach

GraphQL has been 'the future of APIs' for almost a decade. REST has been 'dying' for just as long. Both are still widely used in 2026 - because they solve different problems. Here's a practical framework for choosing, with real tradeoffs from production systems.

How to Use Redis for Caching in PHP: Cutting Response Times from 800ms to 40ms (2015)aunimeda
Backend Engineering

How to Use Redis for Caching in PHP: Cutting Response Times from 800ms to 40ms (2015)

Redis caching reduced our PHP app's average response time from 800ms to 40ms on product listing pages. The pattern: cache database query results with TTL, invalidate on write. Here's the exact implementation with cache key strategy, stampede prevention, and cache warming.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Get Consultation All articles