Engineer PHP REST API from Scratch: Master Manual HTTP Controls

Engineer PHP REST API from Scratch: Master Manual HTTP Controls

Activate your development prowess and embark on a critical expedition: engineering a REST API in PHP from its foundational elements. Modern web applications demand robust, scalable, and interconnected services. The API serves as the vital circulatory system for data exchange, empowering diverse clients—from mobile apps to single-page applications—to interact seamlessly with your backend logic.

This deep dive transforms complex concepts into actionable blueprints, focusing on the crucial, often overlooked details of manually handling HTTP methods, meticulously crafting JSON responses, and deploying precise HTTP status codes. While frameworks abstract much of this complexity, mastering the raw mechanics decodes the true power and flexibility of PHP. This knowledge proves indispensable not only for custom solutions but also for robust problem-solving within existing systems.

We forge a path to understand the core principles that underpin building modern APIs with PHP, ensuring every byte of data transmission, every request, and every response adheres to architectural rigor. Prepare to conquer this frontier, transforming theoretical understanding into practical, deployable code.

Forging the Foundation: Routing and Request Identification

We initiate the API construction by establishing a robust foundation for request handling and routing. Every API begins with the ability to interpret incoming requests, directing them to the correct logical handler. This demands a front controller pattern, where all requests channel through a single PHP script, typically `index.php`. To achieve this, we configure the web server (Apache with a .htaccess file, or Nginx with similar rewrite rules) to redirect all non-file/non-directory requests to our front controller.

Within index.php, we decode two fundamental aspects of the incoming HTTP request: the HTTP method (GET, POST, PUT, DELETE) and the URI (Uniform Resource Identifier). The $_SERVER['REQUEST_METHOD'] superglobal provides the method, while $_SERVER['REQUEST_URI'] delivers the full requested path. We meticulously parse this URI, stripping query strings and base path prefixes to isolate the meaningful resource path (e.g., /products or /users/123). This normalized URI becomes the key identifier for our routing logic.

Our router then, acting as the central traffic controller, employs conditional statements (or a more advanced routing library in larger projects) to map the combination of HTTP method and URI to specific backend operations. For instance, a GET /products request might fetch a list of products, while a POST /products request would create a new product. This initial routing mechanism is paramount; it determines how your API segments its functionality, ensuring each endpoint serves a distinct, predictable purpose.

Understanding and implementing this manual routing mechanism empowers you to design highly specific and efficient API structures, laying the groundwork for precise control over every incoming request. We establish the critical sendJsonResponse function immediately, ensuring all API responses consistently return JSON, setting the stage for predictable data interchange. This architectural decision reinforces the API’s commitment to a uniform communication protocol.

<?php
// .htaccess for URL rewriting
/*
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^ index.php [QSA,L]
*/

// api/index.php - Front Controller
// Define a base path constant to ensure consistent file includes
define('BASE_PATH', __DIR__);

// Set content type for all responses to JSON by default
header('Content-Type: application/json');

// Function to send JSON response with appropriate status code
function sendJsonResponse($data, $statusCode = 200) {
    http_response_code($statusCode);
    echo json_encode($data);
    exit();
}

// Decode the request method
$method = $_SERVER['REQUEST_METHOD'];

// Decode the requested URI and clean it up
// Remove query string and trim slashes
$uri = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');

// Remove any base path prefix if your API is in a subfolder (e.g., /api)
// Adjust this if your actual API base path is different
$apiPrefix = 'api'; // e.g., if your API is accessed via http://yourdomain.com/api/
if (strpos($uri, $apiPrefix) === 0) {
    $uri = substr($uri, strlen($apiPrefix));
    $uri = trim($uri, '/');
}

// Simple Router logic based on URI and method
// We'll manage routes and their associated handlers here

// Example: /products endpoint
if ($uri === 'products') {
    switch ($method) {
        case 'GET':
            // Implement logic to fetch all products
            sendJsonResponse(['message' => 'Fetching all products'], 200);
            break;
        case 'POST':
            // Implement logic to create a new product
            sendJsonResponse(['message' => 'Creating a new product'], 201);
            break;
        default:
            // Method Not Allowed
            sendJsonResponse(['error' => 'Method Not Allowed'], 405);
            break;
    }
} elseif (preg_match('/^products\/(\d+)$/', $uri, $matches)) {
    // Example: /products/{id} endpoint
    $productId = $matches[1];
    switch ($method) {
        case 'GET':
            // Implement logic to fetch a specific product
            sendJsonResponse(['message' => 'Fetching product with ID: ' . $productId], 200);
            break;
        case 'PUT':
            // Implement logic to update a specific product
            sendJsonResponse(['message' => 'Updating product with ID: ' . $productId], 200);
            break;
        case 'DELETE':
            // Implement logic to delete a specific product
            sendJsonResponse(['message' => 'Deleting product with ID: ' . $productId], 204);
            break;
        default:
            // Method Not Allowed
            sendJsonResponse(['error' => 'Method Not Allowed'], 405);
            break;
    }
} else {
    // Not Found
    sendJsonResponse(['error' => 'Not Found'], 404);
}
?>

Deconstructing Requests: Input Parsing and Initial Validation

Engineering a robust API mandates precise handling of incoming data. Requests rarely arrive pristine; they require careful parsing and rigorous validation. For GET requests, input typically resides in the URL's query string ($_GET). However, POST, PUT, and PATCH requests often carry more complex payloads, frequently in JSON format within the request body.

To capture this JSON payload, we directly access the raw input stream using file_get_contents('php://input'). This method retrieves the entire request body as a string, which we then transform into a usable PHP associative array with json_decode($input, true). A critical step here involves error checking the json_decode operation using json_last_error(); an invalid JSON payload necessitates an immediate error response (e.g., HTTP 400 Bad Request).

Beyond parsing, input validation is an impenetrable barrier against malformed data and potential security vulnerabilities. We activate a series of validation checks to ensure that each piece of data conforms to its expected type, format, and constraints. For example, a product name must be a string of a certain length, and a price must be a non-negative integer. Implement dedicated validation functions for common data types and business rules. When validation fails, the API must respond with a clear error message and an appropriate HTTP status code, such as 422 Unprocessable Entity, detailing precisely which fields are invalid.

This disciplined approach to input parsing and validation eliminates ambiguity and fortifies your API against erroneous or malicious data. It's a non-negotiable step in constructing an API that is both reliable and secure. By meticulously defining and enforcing data contracts at the entry point, we ensure the integrity of all subsequent operations. We transition from simply receiving data to actively governing its quality and adherence to defined standards, a critical leverage point in robust API development.

<?php
// api/index.php (continued from previous code block)
// ... (sendJsonResponse function and initial routing setup)

// Function to get JSON input from request body
function getJsonInput() {
    $input = file_get_contents('php://input');
    $data = json_decode($input, true); // true for associative array

    if (json_last_error() !== JSON_ERROR_NONE) {
        // Handle JSON parsing error
        sendJsonResponse(['error' => 'Invalid JSON input'], 400);
    }
    return $data;
}

// Function to validate basic string input
function validateString($value, $minLength = 1, $maxLength = 255) {
    if (!is_string($value)) {
        return false;
    }
    $length = strlen($value);
    return ($length >= $minLength && $length <= $maxLength);
}

// Function to validate basic integer input
function validateInt($value, $min = null, $max = null) {
    if (!is_numeric($value) || !is_int((int)$value)) {
        return false;
    }
    $intValue = (int)$value;
    if ($min !== null && $intValue < $min) {
        return false;
    }
    if ($max !== null && $intValue > $max) {
        return false;
    }
    return true;
}

// Example usage within our router for POST/PUT requests

if ($uri === 'products') {
    switch ($method) {
        // ... (GET handler)
        case 'POST':
            $requestData = getJsonInput();
            $errors = [];

            // Basic validation example
            if (!isset($requestData['name']) || !validateString($requestData['name'], 3, 100)) {
                $errors['name'] = 'Product name is required and must be between 3 and 100 characters.';
            }
            if (!isset($requestData['price']) || !validateInt($requestData['price'], 0)) {
                $errors['price'] = 'Product price is required and must be a non-negative integer.';
            }

            if (!empty($errors)) {
                sendJsonResponse(['error' => 'Validation Failed', 'details' => $errors], 422); // Unprocessable Entity
            }

            // Assume validation passed, proceed to create product
            // In a real application, you'd interact with a database here
            $newProduct = [
                'id' => uniqid(), // Simulate new ID
                'name' => $requestData['name'],
                'price' => $requestData['price'],
                'created_at' => date('Y-m-d H:i:s')
            ];
            sendJsonResponse(['message' => 'Product created successfully', 'product' => $newProduct], 201);
            break;
        // ... (default handler)
    }
} elseif (preg_match('/^products\/(\d+)$/', $uri, $matches)) {
    $productId = $matches[1];
    switch ($method) {
        // ... (GET, DELETE handlers)
        case 'PUT':
            $requestData = getJsonInput();
            $errors = [];

            // Basic validation example for update
            if (isset($requestData['name']) && !validateString($requestData['name'], 3, 100)) {
                $errors['name'] = 'Product name must be between 3 and 100 characters.';
            }
            if (isset($requestData['price']) && !validateInt($requestData['price'], 0)) {
                $errors['price'] = 'Product price must be a non-negative integer.';
            }

            if (!empty($errors)) {
                sendJsonResponse(['error' => 'Validation Failed', 'details' => $errors], 422);
            }

            // Assume validation passed, proceed to update product
            // In a real application, you'd interact with a database here
            sendJsonResponse(['message' => 'Product ' . $productId . ' updated successfully', 'data' => $requestData], 200);
            break;
        // ... (default handler)
    }
}
// ... (Not Found handler)
?>

Crafting Responses: Precision in JSON and Status Codes

A REST API's contract with its consumers hinges on clear, consistent responses. This means meticulously crafting JSON payloads and associating them with the correct HTTP status codes. The Content-Type: application/json header is paramount; it informs the client that the response body contains JSON data, enabling proper parsing.

We leverage json_encode() to transform PHP arrays or objects into JSON strings. This function is robust, but developers must ensure the data structure is consistent across similar endpoints. For instance, a successful list of products should always be an array of product objects, not sometimes an object containing an array. Consistency minimizes client-side parsing errors and improves developer experience.

The HTTP status code is a non-verbal yet highly expressive communication channel. It immediately signals the outcome of an API request without requiring the client to parse the response body. We decode the significance of key status codes:

  • 200 OK: The request succeeded, and data is returned (e.g., a GET request for a single resource).
  • 201 Created: A new resource has been successfully created (e.g., a POST request). The response should ideally include the newly created resource and a Location header pointing to its URI.
  • 204 No Content: The request succeeded, but no content is returned (e.g., a successful DELETE request). The response body must be empty.
  • 400 Bad Request: The client sent an invalid request (e.g., malformed JSON).
  • 404 Not Found: The requested resource does not exist.
  • 405 Method Not Allowed: The HTTP method used is not supported for the requested resource.
  • 500 Internal Server Error: A generic server-side error occurred.

By precisely deploying these status codes with http_response_code(), we guide API consumers through the flow of interaction, ensuring every operation's outcome is immediately understandable. This meticulous attention to response structure and status codes elevates an API from a functional script to a truly professional, communicative service, empowering seamless integration and robust client-side error handling.

<?php
// api/index.php (continued from previous code block)
// ... (sendJsonResponse function, router, and input handling)

// We've already defined sendJsonResponse earlier:
/*
function sendJsonResponse($data, $statusCode = 200) {
    header('Content-Type: application/json');
    http_response_code($statusCode);
    echo json_encode($data);
    exit();
}
*/

// Example of how different API endpoints leverage sendJsonResponse

// Simulated data store (replace with actual database interaction)
$products = [
    'prod-1' => ['id' => 'prod-1', 'name' => 'Laptop', 'price' => 1200],
    'prod-2' => ['id' => 'prod-2', 'name' => 'Mouse', 'price' => 25]
];

// Router logic refinement:
if ($uri === 'products') {
    switch ($method) {
        case 'GET':
            // Fetch all products
            sendJsonResponse(array_values($products), 200); // 200 OK
            break;
        case 'POST':
            $requestData = getJsonInput();
            $errors = [];

            if (!isset($requestData['name']) || !validateString($requestData['name'], 3, 100)) {
                $errors['name'] = 'Product name is required and must be between 3 and 100 characters.';
            }
            if (!isset($requestData['price']) || !validateInt($requestData['price'], 0)) {
                $errors['price'] = 'Product price is required and must be a non-negative integer.';
            }

            if (!empty($errors)) {
                sendJsonResponse(['error' => 'Validation Failed', 'details' => $errors], 422);
            }

            $newId = 'prod-' . (count($products) + 1);
            $newProduct = [
                'id' => $newId,
                'name' => $requestData['name'],
                'price' => $requestData['price'],
                'created_at' => date('Y-m-d H:i:s')
            ];
            $products[$newId] = $newProduct; // Add to simulated store

            // Respond with the newly created resource and 201 Created
            sendJsonResponse(['message' => 'Product created successfully', 'product' => $newProduct], 201);
            break;
        default:
            sendJsonResponse(['error' => 'Method Not Allowed'], 405);
            break;
    }
} elseif (preg_match('/^products\/([a-zA-Z0-9\-]+)$/', $uri, $matches)) {
    $productId = $matches[1];
    switch ($method) {
        case 'GET':
            if (isset($products[$productId])) {
                sendJsonResponse($products[$productId], 200);
            } else {
                sendJsonResponse(['error' => 'Product not found'], 404); // 404 Not Found
            }
            break;
        case 'PUT':
            $requestData = getJsonInput();
            $errors = [];

            if (isset($requestData['name']) && !validateString($requestData['name'], 3, 100)) {
                $errors['name'] = 'Product name must be between 3 and 100 characters.';
            }
            if (isset($requestData['price']) && !validateInt($requestData['price'], 0)) {
                $errors['price'] = 'Product price must be a non-negative integer.';
            }

            if (!empty($errors)) {
                sendJsonResponse(['error' => 'Validation Failed', 'details' => $errors], 422);
            }

            if (isset($products[$productId])) {
                // Update existing product in simulated store
                $products[$productId] = array_merge($products[$productId], $requestData);
                sendJsonResponse(['message' => 'Product updated successfully', 'product' => $products[$productId]], 200);
            } else {
                // If PUT is used for creation, respond with 201, otherwise 404
                // For this example, we assume PUT is for updating existing resources.
                sendJsonResponse(['error' => 'Product not found for update'], 404);
            }
            break;
        case 'DELETE':
            if (isset($products[$productId])) {
                unset($products[$productId]); // Remove from simulated store
                sendJsonResponse(null, 204); // 204 No Content for successful deletion
            } else {
                sendJsonResponse(['error' => 'Product not found for deletion'], 404);
            }
            break;
        default:
            sendJsonResponse(['error' => 'Method Not Allowed'], 405);
            break;
    }
}
// ... (Not Found handler)
?>

Activating Persistence and Comprehensive Error Management

A functional API must not only process requests but also persistently store and retrieve data. While a full-fledged database integration (e.g., MySQL with PDO) is beyond this initial scratch build, we can activate a simulated data persistence layer using a simple PHP class acting as a 'repository.' This abstracts the data access logic, making the code cleaner and easier to transition to a real database later. This repository encapsulates operations like finding, saving, updating, and deleting entities, mimicking real-world data interactions without the complexities of SQL at this stage.

Equally critical is a comprehensive error management strategy. Unforeseen issues—like database connection failures, invalid logic, or unexpected exceptions—must not crash the API silently or expose sensitive server details. We establish custom error and exception handlers using set_error_handler() and set_exception_handler(). These global handlers intercept any PHP errors or uncaught exceptions, preventing default PHP error pages from being displayed.

Instead, our handlers transform these internal server errors into consistent, client-friendly JSON responses with an HTTP 500 Internal Server Error status code. The response payload should include a generic error message (e.g., "Server Error") and, crucially, avoid exposing internal paths, database queries, or stack traces in a production environment. For development, including file and line numbers can aid debugging. This structured approach to error handling ensures your API always communicates clearly, even when facing internal turmoil, maintaining a professional interface regardless of backend complications.

By integrating a data persistence layer (even a simulated one) and implementing robust error handling, we elevate the API from a mere request processor to a resilient, enterprise-ready service. These practices are fundamental to building an API that is both reliable under heavy load and secure against common operational failures, fortifying its operational integrity against the unpredictable nature of real-world application environments.

<?php
// api/index.php (continued from previous code block)
// ... (sendJsonResponse function, router, input handling, and existing product data)

// --- Database Simulation (for demonstration) ---
class ProductRepository {
    private $products = [];

    public function __construct() {
        // Load some initial data
        $this->products = [
            'prod-1' => ['id' => 'prod-1', 'name' => 'Laptop', 'price' => 1200, 'created_at' => '2023-01-01 10:00:00'],
            'prod-2' => ['id' => 'prod-2', 'name' => 'Mouse', 'price' => 25, 'created_at' => '2023-01-02 11:00:00']
        ];
    }

    public function findAll() {
        return array_values($this->products);
    }

    public function findById($id) {
        return $this->products[$id] ?? null;
    }

    public function save($product) {
        if (!isset($product['id'])) {
            $product['id'] = 'prod-' . (count($this->products) + 1); // Generate new ID
            $product['created_at'] = date('Y-m-d H:i:s');
        }
        $this->products[$product['id']] = $product;
        return $product;
    }

    public function update($id, $data) {
        if (!isset($this->products[$id])) {
            return null; // Not found
        }
        $this->products[$id] = array_merge($this->products[$id], $data);
        return $this->products[$id];
    }

    public function delete($id) {
        if (isset($this->products[$id])) {
            unset($this->products[$id]);
            return true;
        }
        return false;
    }
}

$productRepository = new ProductRepository();

// --- Error Handling Enhancement ---
// Set custom error and exception handlers to catch unexpected issues
set_error_handler(function ($severity, $message, $file, $line) {
    // Ignore suppressed errors (e.g., @ operator)
    if (!(error_reporting() & $severity)) {
        return false;
    }
    sendJsonResponse(['error' => 'Server Error', 'message' => $message, 'file' => $file, 'line' => $line], 500);
});

set_exception_handler(function ($exception) {
    sendJsonResponse(['error' => 'Server Exception', 'message' => $exception->getMessage(), 'file' => $exception->getFile(), 'line' => $exception->getLine()], 500);
});


// Router logic leveraging the repository:
// Update existing router logic to use $productRepository

if ($uri === 'products') {
    switch ($method) {
        case 'GET':
            sendJsonResponse($productRepository->findAll(), 200);
            break;
        case 'POST':
            $requestData = getJsonInput();
            $errors = [];

            if (!isset($requestData['name']) || !validateString($requestData['name'], 3, 100)) {
                $errors['name'] = 'Product name is required and must be between 3 and 100 characters.';
            }
            if (!isset($requestData['price']) || !validateInt($requestData['price'], 0)) {
                $errors['price'] = 'Product price is required and must be a non-negative integer.';
            }

            if (!empty($errors)) {
                sendJsonResponse(['error' => 'Validation Failed', 'details' => $errors], 422);
            }

            $newProduct = $productRepository->save($requestData);
            sendJsonResponse(['message' => 'Product created successfully', 'product' => $newProduct], 201);
            break;
        default:
            sendJsonResponse(['error' => 'Method Not Allowed'], 405);
            break;
    }
} elseif (preg_match('/^products\/([a-zA-Z0-9\-]+)$/', $uri, $matches)) {
    $productId = $matches[1];
    switch ($method) {
        case 'GET':
            $product = $productRepository->findById($productId);
            if ($product) {
                sendJsonResponse($product, 200);
            } else {
                sendJsonResponse(['error' => 'Product not found'], 404);
            }
            break;
        case 'PUT':
            $requestData = getJsonInput();
            $errors = [];

            if (isset($requestData['name']) && !validateString($requestData['name'], 3, 100)) {
                $errors['name'] = 'Product name must be between 3 and 100 characters.';
            }
            if (isset($requestData['price']) && !validateInt($requestData['price'], 0)) {
                $errors['price'] = 'Product price must be a non-negative integer.';
            }

            if (!empty($errors)) {
                sendJsonResponse(['error' => 'Validation Failed', 'details' => $errors], 422);
            }

            $updatedProduct = $productRepository->update($productId, $requestData);
            if ($updatedProduct) {
                sendJsonResponse(['message' => 'Product updated successfully', 'product' => $updatedProduct], 200);
            } else {
                sendJsonResponse(['error' => 'Product not found for update'], 404);
            }
            break;
        case 'DELETE':
            if ($productRepository->delete($productId)) {
                sendJsonResponse(null, 204);
            } else {
                sendJsonResponse(['error' => 'Product not found for deletion'], 404);
            }
            break;
        default:
            sendJsonResponse(['error' => 'Method Not Allowed'], 405);
            break;
    }
}
// ... (Not Found handler)

// Final fallback for any unhandled routes
sendJsonResponse(['error' => 'Not Found'], 404);
?>

Optimizing and Securing the Frontier: Next Steps and Best Practices

As we engineer our API, merely achieving functionality is insufficient; we must optimize for performance, resilience, and security. Activating these crucial layers transforms a basic API into a production-grade service. While the scratch build focuses on core mechanics, understanding these next steps is vital.

Security is not an afterthought; it's an integrated process. Implement robust

  • Authentication: While frameworks handle JWTs or OAuth, a custom API might start with simple API keys (transmitted via headers like X-API-KEY) or HTTP Basic Authentication. Crucially, API keys must be stored securely and validated meticulously.
  • Authorization: Beyond identifying the user, determine if they have permission to perform specific actions on resources.
  • Input Sanitization: Every piece of user-provided data must be sanitized to prevent XSS (Cross-Site Scripting) and SQL injection. Use functions like htmlspecialchars() for output and prepared statements with PDO for database interactions.
  • HTTPS: Always enforce HTTPS to encrypt data in transit, protecting against eavesdropping and man-in-the-middle attacks.
  • Security Headers: Deploy HTTP security headers (e.g., X-Content-Type-Options, X-Frame-Options, Content-Security-Policy) to mitigate common web vulnerabilities.

Performance Optimization ensures your API remains responsive under load:

  • Caching: Implement caching for frequently accessed, immutable data using tools like Redis or Memcached.
  • Database Indexing: Proper indexing on database tables dramatically speeds up query execution.
  • Efficient Queries: Optimize SQL queries to retrieve only necessary data, avoiding N+1 problems.
  • Rate Limiting: Protect your API from abuse and DDoS attacks by limiting the number of requests a single client can make within a given timeframe.

By proactively considering these optimizations and security measures, we forge an API that is not only functional but also stable, secure, and scalable. This strategic foresight transforms a proof-of-concept into a dependable computational explorer, ready to navigate the complex landscape of modern software development and stand resilient against the evolving threat landscape.

<?php
// api/index.php (continued from previous code block)
// ... (All previous code sections: sendJsonResponse, repository, error handlers, router)

// --- Final Check: Ensure the 'Not Found' handler is robust ---
// This line ensures that if no route matches, a 404 is always sent.
// It should be the very last line of the router logic.
// sendJsonResponse(['error' => 'Not Found'], 404);

// --- Common Security Headers (Add at the very beginning of index.php, after <?php) ---
// header('X-Content-Type-Options: nosniff');
// header('X-Frame-Options: DENY');
// header('X-XSS-Protection: 1; mode=block');
// header('Content-Security-Policy: default-src 'self'');

// --- Simple API Key Authentication Example (Basic, for demonstration) ---
// This would typically come before routing, but after setup.
/*
function authenticateRequest() {
    $apiKey = $_SERVER['HTTP_X_API_KEY'] ?? ''; // Expect API key in X-API-KEY header
    $validApiKey = 'YOUR_SECRET_API_KEY'; // Replace with a real, securely stored key

    if ($apiKey !== $validApiKey) {
        sendJsonResponse(['error' => 'Unauthorized: Invalid API Key'], 401);
    }
    // If valid, continue processing
}

// Call authentication at the start of your front controller
// authenticateRequest();
*/

// --- Input Sanitization Example (Consider for all user-provided data) ---
// Function to sanitize string input against XSS
function sanitizeString($value) {
    return htmlspecialchars(trim($value), ENT_QUOTES, 'UTF-8');
}

// Example usage within the POST/PUT product endpoint validation:
/*
            // ... inside POST or PUT product handling ...
            $requestData['name'] = sanitizeString($requestData['name']);
            // ... then proceed with validation and saving ...
*/

// --- Rate Limiting Concept (Requires more advanced setup, e.g., Redis or database) ---
// Prevents abuse by limiting the number of requests a user/IP can make in a given time.
// Example logic (conceptual):
/*
function checkRateLimit($ipAddress, $maxRequests = 100, $windowSeconds = 3600) {
    // In a real scenario, this would interact with a persistent store (Redis, DB)
    // to track request counts for the $ipAddress within $windowSeconds.
    // If limit exceeded, sendJsonResponse(['error' => 'Too Many Requests'], 429);
    return true; // For demonstration, assume no rate limit
}
// Call at the start: checkRateLimit($_SERVER['REMOTE_ADDR']);
*/

?>

Key Takeaways

API Foundation: Routing & Request Identification

Engineer a front controller (index.php) and .htaccess to funnel all requests. Decode HTTP methods ($_SERVER['REQUEST_METHOD']) and URIs ($_SERVER['REQUEST_URI']) to direct requests to appropriate handlers. Implement sendJsonResponse early for consistent output.

Input Mastery: Parsing & Validation

Extract raw JSON input using file_get_contents('php://input') and json_decode(), ensuring error checks. Activate robust validation functions for all incoming data to enforce data contracts, responding with 422 Unprocessable Entity for invalid payloads.

Response Precision: JSON & Status Codes

Craft consistent JSON responses using json_encode(). Precisely deploy HTTP status codes (200, 201, 204, 400, 404, 405, 500) via http_response_code() and header('Content-Type: application/json') to clearly communicate API outcomes to clients.

System Resilience: Persistence & Error Handling

Integrate a data persistence layer (even simulated initially) to manage resource state. Implement global error and exception handlers (set_error_handler(), set_exception_handler()) to intercept unexpected issues and consistently respond with 500 Internal Server Error JSON payloads, preventing information leakage.

Strategic Expansion: Security & Optimization

Prioritize security from inception: enforce HTTPS, sanitize inputs (XSS/SQLi), implement authentication/authorization, and deploy HTTP security headers. Plan for performance optimization with caching, efficient queries, and rate limiting to build a scalable and robust API.

FAQ

  • Why build a REST API from scratch in PHP when frameworks exist?

    Building from scratch provides profound understanding of fundamental HTTP protocols, request/response cycles, and architectural principles. This mastery enables more efficient debugging, optimization, and customization, even when eventually working with frameworks. It decodes the 'magic' of frameworks, transforming you into a true development strategist.

  • What is the importance of HTTP status codes in a REST API?

    HTTP status codes are vital for clear communication. They provide immediate, standardized feedback on the outcome of a request without needing to parse the response body. They guide clients on how to proceed, distinguish between success, client-side errors, and server-side errors, and are crucial for robust error handling and client application logic.

  • How do you handle different HTTP methods (GET, POST, PUT, DELETE) in a single PHP file?

    We utilize the $_SERVER['REQUEST_METHOD'] superglobal to identify the incoming HTTP method. A front controller script then uses conditional logic (e.g., a switch statement) to route the request based on both the URI and the HTTP method to the appropriate handler function or block of code. This ensures each method for a given resource triggers distinct server-side operations.

  • What are common security considerations when building a custom PHP API?

    Key considerations include: enforcing HTTPS, sanitizing all input to prevent XSS and SQL injection, implementing robust authentication (API keys, OAuth, JWT) and authorization, rate limiting to prevent abuse, and configuring security-related HTTP headers (e.g., CSP, X-Frame-Options). Never expose sensitive information in error messages or logs.

  • How can I scale this basic API further?

    Scaling involves several steps: transitioning from file-based or array-based data to a proper database (e.g., MySQL with PDO), implementing a more sophisticated routing system (using a dedicated routing library), introducing caching mechanisms (Redis/Memcached), adopting asynchronous processing for long-running tasks, and containerizing your application with Docker for easier deployment and scaling.