Skip to main content
API Security Tutorial
CHAPTER 19 Intermediate

Building a Complete Secure REST API Project

Updated: May 13, 2026
35 min read

# CHAPTER 19

Building a Complete Secure REST API Project

1. Introduction

Knowledge without application is just trivia. In this capstone chapter, we will synthesize all 18 previous chapters to architect the core logic of a Secure E-Commerce REST API. We won't write thousands of lines of boilerplate; instead, we will focus on the exact code blocks required to implement the critical security checkpoints: HTTPS enforcement, routing, rate limiting, JWT authentication, authorization (BOLA prevention), input validation, prepared statements, and secure logging.

2. Learning Objectives

By the end of this chapter, you will be able to:
  • Trace the lifecycle of a secure API request from start to finish.
  • Synthesize multiple security concepts (Rate Limiting, JWT, PDO) into a single flow.
  • Understand how to logically structure a secure endpoint.
  • Prepare a secure codebase for production deployment.

3. The Project Scenario

The App: SecureShop API The Endpoint: PUT /api/orders/105 (Updating the shipping address for an order).

The Security Requirements:

  1. 1. Reject non-HTTPS traffic.
  1. 2. Prevent brute-force abuse (Rate Limiting).
  1. 3. Ensure the user is logged in (Authentication/JWT).
  1. 4. Ensure the user *owns* order 105 (Authorization/BOLA).
  1. 5. Ensure the new address is safe text (Validation).
  1. 6. Update the database safely (SQLi Prevention).
  1. 7. Log the update for auditing.

4. Step 1: The Front Controller & Global Defenses

Everything hits index.php. We set our headers and enforce the baseline.
php
123456789101112131415161718192021222324252627282930313233
<?php
declare(strict_types=1);
require &#039;vendor/autoload.php'; // Loads JWT libraries, etc.
require &#039;config/database.php';

// 1. Secure Headers (XSS and Mime-Sniffing prevention)
header(&#039;Content-Type: application/json; charset=utf-8');
header("X-Content-Type-Options: nosniff");
header("Content-Security-Policy: default-src &#039;none'");

// 2. HTTPS Enforcement (Chapter 3)
if (!isset($_SERVER[&#039;HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
    http_response_code(403);
    die(json_encode(["error" => "HTTPS required."]));
}

// 3. CORS Preflight Handling (Chapter 12)
$allowed_origins = [&#039;https://www.secureshop.com'];
$origin = $_SERVER[&#039;HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed_origins)) {
    header("Access-Control-Allow-Origin: " . $origin);
    header("Access-Control-Allow-Methods: GET, PUT, OPTIONS");
    header("Access-Control-Allow-Headers: Authorization, Content-Type");
}
if ($_SERVER[&#039;REQUEST_METHOD'] === 'OPTIONS') { exit; }

// 4. Rate Limiting (Chapter 13)
// (Conceptual logic utilizing Redis)
if (check_rate_limit($_SERVER[&#039;REMOTE_ADDR']) > 100) {
    http_response_code(429);
    die(json_encode(["error" => "Too Many Requests."]));
}
?>

5. Step 2: Authentication Middleware

Before we route to the Order logic, we must verify identity.
php
123456789101112131415161718192021222324252627
<?php
// Extract routing info
$method = $_SERVER[&#039;REQUEST_METHOD'];
$path = parse_url($_SERVER[&#039;REQUEST_URI'], PHP_URL_PATH);

$logged_in_user_id = null;

// Require Auth for all /api/orders routes (Chapter 4 & 6)
if (strpos($path, &#039;/api/orders') === 0) {
    $headers = apache_request_headers();
    if (!isset($headers[&#039;Authorization'])) {
        http_response_code(401);
        die(json_encode(["error" => "Unauthorized. Missing Token."]));
    }
    
    $token = str_replace(&#039;Bearer ', '', $headers['Authorization']);
    
    try {
        // Vetted library validates Signature and Expiration!
        $decoded = \Firebase\JWT\JWT::decode($token, new \Firebase\JWT\Key($_ENV[&#039;JWT_SECRET'], 'HS256'));
        $logged_in_user_id = $decoded->sub; // Subject = User ID
    } catch (Exception $e) {
        http_response_code(401);
        die(json_encode(["error" => "Invalid or expired token."]));
    }
}
?>

6. Step 3: Input Validation

The request is routed to the PUT /orders controller. The user wants to update the address.
php
1234567891011121314151617
<?php
// We matched the route: PUT /api/orders/{id}
// Extract ID from URL using regex
preg_match(&#039;/\/api\/orders\/(\d+)/', $path, $matches);
$order_id = (int) $matches[1];

// Extract JSON body
$raw_data = file_get_contents("php://input");
$input = json_decode($raw_data, true);

// Validation (Chapter 9)
if (!isset($input[&#039;address']) || !is_string($input['address']) || strlen($input['address']) > 200) {
    http_response_code(400);
    die(json_encode(["error" => "Bad Request. Address must be a string under 200 chars."]));
}
$new_address = $input[&#039;address'];
?>

7. Step 4: Authorization (BOLA Mitigation) & SQL Execution

We have the clean $new_address. We must update the database, but we MUST check ownership.
php
123456789101112131415161718192021222324252627282930313233343536
<?php
try {
    $pdo = Database::getConnection();
    
    // Authorization + SQLi Prevention (Chapters 8 & 10)
    // The query explicitly demands that the owner_id matches the authenticated user!
    $stmt = $pdo->prepare("UPDATE orders SET shipping_address = :addr WHERE id = :order_id AND user_id = :user_id");
    
    $stmt->execute([
        &#039;:addr'     => $new_address,
        &#039;:order_id' => $order_id,
        &#039;:user_id'  => $logged_in_user_id // Guaranteed by JWT
    ]);
    
    // Check if the row was actually updated
    if ($stmt->rowCount() === 0) {
        // The order doesn't exist, OR it belongs to someone else.
        // Return 404 to avoid leaking existence of other users' orders.
        http_response_code(404);
        die(json_encode(["error" => "Order not found or permission denied."]));
    }
    
    // Audit Logging (Chapter 16)
    error_log("AUDIT: User {$logged_in_user_id} updated Order {$order_id}");
    
    // Success Response
    http_response_code(200);
    echo json_encode(["message" => "Order address updated successfully."]);

} catch (PDOException $e) {
    // Secure Error Handling (Chapter 15)
    error_log("DB Error: " . $e->getMessage()); // Private log
    http_response_code(500);
    echo json_encode(["error" => "Internal Server Error."]); // Public generic message
}
?>

8. Architectural Review

Look at the flow we just built:
  1. 1. The attacker cannot access it without HTTPS.
  1. 2. The attacker cannot brute force it (Rate Limit).
  1. 3. The attacker cannot forge a login (JWT Signature check).
  1. 4. The attacker cannot send massive/malformed JSON (Validation).
  1. 5. The attacker cannot modify someone else's order (BOLA SQL check).
  1. 6. The attacker cannot inject SQL code (PDO Prepared Statements).
  1. 7. If the attacker tries and fails, it is quietly logged (Auditing).

This is what a production-grade, battle-tested API looks like.

9. Deployment Best Practices

Before pushing this code to a live server:
  • Ensure php.ini has display_errors = Off.
  • Ensure your .env file containing the database password and JWT Secret is not pushed to GitHub.
  • Set up automated SSL certificate renewal (e.g., Certbot/Let's Encrypt).
  • Ensure the uploads/ directory (if you have one) is locked down.

10. Summary

In this capstone chapter, we wrote the code for a complete, secure API request lifecycle. By layering defenses—Front Controller headers, Authentication Middleware, strict Input Validation, and Ownership-verifying Prepared Statements—we built an endpoint capable of withstanding the OWASP Top 10. Security is not a single line of code; it is a philosophy of "Defense in Depth."

11. Next Chapter Recommendation

You have mastered the architecture and the code. Now it is time to prove it to employers. Proceed to Chapter 20: API Security Interview Questions and Practice Challenges to prepare for your cybersecurity or backend engineering interviews.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·