Project Overview

A RESTful API allows different applications to communicate over the web using standard HTTP methods. This project covers creating a simple RESTful API using PHP and MySQL, enabling operations like fetching, creating, updating, and deleting data. This foundational skill is essential for developing modern web applications, mobile apps, and integrating with third-party services.

Prerequisites

Ensure you have the following:

  • Web Server: Apache (using XAMPP, WAMP, or MAMP)
  • PHP: Version 7.4 or higher
  • MySQL: For database management
  • Code Editor: VS Code, Sublime Text, PHPStorm, etc.
  • Composer: For dependency management (optional)
  • Basic Understanding of HTTP Methods, JSON, and CRUD Operations in PHP

Step-by-Step Procedure

1. Setting Up the Development Environment

  1. Install XAMPP:
    • Download from XAMPP Official Website.
    • Follow the installation wizard and install it in the default directory.
  2. Start Apache and MySQL:
    • Open the XAMPP Control Panel.
    • Start the Apache and MySQL modules.
  3. Install Composer (Optional):

2. Creating the Database

  1. Access phpMyAdmin:
    • Navigate to http://localhost/phpmyadmin/ in your browser.
  2. Create a New Database:
    • Click on “New” in the left sidebar.
    • Name the database rest_api.
    • Choose “utf8mb4_unicode_ci” as the collation.
    • Click “Create”.
  3. Create a books Table:
    • Select the rest_api database.
    • Click on “New” to create a table.
    • Define the table with the following fields:
    Field Type Null Key Default Extra id INT NO PRI NULL AUTO_INCREMENT title VARCHAR(255) NO NULL author VARCHAR(255) NO NULL isbn VARCHAR(20) NO UNI NULL published_at DATE YES NULL created_at TIMESTAMP NO CURRENT_TIMESTAMP
    • Click “Save”.

3. Project Structure

Organize your project files as follows:

rest-api/
├── vendor/             # Composer dependencies (if any)
├── config/
│   └── db.php
├── api/
│   ├── index.php
│   ├── .htaccess
├── src/
│   ├── Book.php
│   └── BookController.php
├── composer.json
├── README.md

4. Configuration

a. Database Connection (config/db.php)

Create a file named db.php inside the config directory to handle database connections.

<?php
// config/db.php

$host = 'localhost';
$db   = 'rest_api';
$user = 'root'; // Default XAMPP MySQL user
$pass = '';     // Default XAMPP MySQL password is empty
$charset = 'utf8mb4';

$dsn = "mysql:host=$host;dbname=$db;charset=$charset";

$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, // Enable exceptions
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,       // Fetch associative arrays
    PDO::ATTR_EMULATE_PREPARES   => false,                  // Disable emulation
];

try {
    $pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
    // Handle connection errors
    http_response_code(500);
    echo json_encode(['error' => 'Database connection failed.']);
    exit;
}
?>
b. URL Rewriting (api/.htaccess)

To route all API requests to index.php, create an .htaccess file inside the api directory.

# api/.htaccess

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ index.php [QSA,L]

5. Creating the Book Model (src/Book.php)

Create a Book.php file inside the src directory to represent the Book entity.

<?php
// src/Book.php

class Book {
    public $id;
    public $title;
    public $author;
    public $isbn;
    public $published_at;

    public function __construct($data) {
        $this->id = (int)$data['id'] ?? null;
        $this->title = $data['title'] ?? '';
        $this->author = $data['author'] ?? '';
        $this->isbn = $data['isbn'] ?? '';
        $this->published_at = $data['published_at'] ?? null;
    }

    public function toArray() {
        return [
            'id'           => $this->id,
            'title'        => $this->title,
            'author'       => $this->author,
            'isbn'         => $this->isbn,
            'published_at' => $this->published_at,
        ];
    }
}
?>

6. Creating the Book Controller (src/BookController.php)

Create a BookController.php file inside the src directory to handle API requests related to books.

<?php
// src/BookController.php

require_once __DIR__ . '/../config/db.php';
require_once 'Book.php';

class BookController {
    private $pdo;

    public function __construct($pdo) {
        $this->pdo = $pdo;
    }

    // GET /api/books
    public function getAllBooks() {
        $stmt = $this->pdo->query('SELECT * FROM books');
        $books = [];
        while ($row = $stmt->fetch()) {
            $book = new Book($row);
            $books[] = $book->toArray();
        }
        echo json_encode($books);
    }

    // GET /api/books/{id}
    public function getBook($id) {
        $stmt = $this->pdo->prepare('SELECT * FROM books WHERE id = ?');
        $stmt->execute([$id]);
        $row = $stmt->fetch();
        if ($row) {
            $book = new Book($row);
            echo json_encode($book->toArray());
        } else {
            http_response_code(404);
            echo json_encode(['error' => 'Book not found.']);
        }
    }

    // POST /api/books
    public function createBook() {
        $data = json_decode(file_get_contents('php://input'), true);
        if (!$data) {
            http_response_code(400);
            echo json_encode(['error' => 'Invalid JSON data.']);
            return;
        }

        // Validate required fields
        if (empty($data['title']) || empty($data['author']) || empty($data['isbn'])) {
            http_response_code(400);
            echo json_encode(['error' => 'Title, Author, and ISBN are required.']);
            return;
        }

        // Insert into database
        $stmt = $this->pdo->prepare('INSERT INTO books (title, author, isbn, published_at) VALUES (?, ?, ?, ?)');
        try {
            $stmt->execute([
                $data['title'],
                $data['author'],
                $data['isbn'],
                $data['published_at'] ?? null
            ]);
            $book_id = $this->pdo->lastInsertId();
            $this->getBook($book_id);
        } catch (PDOException $e) {
            if ($e->getCode() == 23000) { // Integrity constraint violation
                http_response_code(409);
                echo json_encode(['error' => 'ISBN already exists.']);
            } else {
                http_response_code(500);
                echo json_encode(['error' => 'Failed to create book.']);
            }
        }
    }

    // PUT /api/books/{id}
    public function updateBook($id) {
        $data = json_decode(file_get_contents('php://input'), true);
        if (!$data) {
            http_response_code(400);
            echo json_encode(['error' => 'Invalid JSON data.']);
            return;
        }

        // Check if book exists
        $stmt = $this->pdo->prepare('SELECT * FROM books WHERE id = ?');
        $stmt->execute([$id]);
        $row = $stmt->fetch();
        if (!$row) {
            http_response_code(404);
            echo json_encode(['error' => 'Book not found.']);
            return;
        }

        // Update fields if provided
        $title = $data['title'] ?? $row['title'];
        $author = $data['author'] ?? $row['author'];
        $isbn = $data['isbn'] ?? $row['isbn'];
        $published_at = $data['published_at'] ?? $row['published_at'];

        // Update in database
        $stmt = $this->pdo->prepare('UPDATE books SET title = ?, author = ?, isbn = ?, published_at = ? WHERE id = ?');
        try {
            $stmt->execute([$title, $author, $isbn, $published_at, $id]);
            $this->getBook($id);
        } catch (PDOException $e) {
            if ($e->getCode() == 23000) { // Integrity constraint violation
                http_response_code(409);
                echo json_encode(['error' => 'ISBN already exists.']);
            } else {
                http_response_code(500);
                echo json_encode(['error' => 'Failed to update book.']);
            }
        }
    }

    // DELETE /api/books/{id}
    public function deleteBook($id) {
        // Check if book exists
        $stmt = $this->pdo->prepare('SELECT * FROM books WHERE id = ?');
        $stmt->execute([$id]);
        $row = $stmt->fetch();
        if (!$row) {
            http_response_code(404);
            echo json_encode(['error' => 'Book not found.']);
            return;
        }

        // Delete from database
        $stmt = $this->pdo->prepare('DELETE FROM books WHERE id = ?');
        if ($stmt->execute([$id])) {
            http_response_code(204); // No Content
        } else {
            http_response_code(500);
            echo json_encode(['error' => 'Failed to delete book.']);
        }
    }
}
?>

7. Handling API Requests (api/index.php)

Create an index.php file inside the api directory to handle incoming API requests.

<?php
// api/index.php

header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Origin: *"); // Adjust this in production for security
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");

// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    exit;
}

require_once __DIR__ . '/../src/BookController.php';

$bookController = new BookController($pdo);

// Parse the URL to determine the endpoint and parameters
$requestMethod = $_SERVER['REQUEST_METHOD'];
$requestUri = $_SERVER['REQUEST_URI'];

// Remove query string from URI
if (strpos($requestUri, '?') !== false) {
    $requestUri = strstr($requestUri, '?', true);
}

// Extract the path after /api/
$path = str_replace('/api/', '', $requestUri);
$path = trim($path, '/');

// Split the path into segments
$pathSegments = explode('/', $path);

// Route the request
if ($pathSegments[0] === 'books') {
    // Determine if an ID is provided
    $id = isset($pathSegments[1]) && is_numeric($pathSegments[1]) ? (int)$pathSegments[1] : null;

    switch ($requestMethod) {
        case 'GET':
            if ($id) {
                $bookController->getBook($id);
            } else {
                $bookController->getAllBooks();
            }
            break;
        case 'POST':
            $bookController->createBook();
            break;
        case 'PUT':
            if ($id) {
                $bookController->updateBook($id);
            } else {
                http_response_code(400);
                echo json_encode(['error' => 'Book ID is required for updating.']);
            }
            break;
        case 'DELETE':
            if ($id) {
                $bookController->deleteBook($id);
            } else {
                http_response_code(400);
                echo json_encode(['error' => 'Book ID is required for deletion.']);
            }
            break;
        default:
            http_response_code(405);
            echo json_encode(['error' => 'Method Not Allowed']);
            break;
    }
} else {
    http_response_code(404);
    echo json_encode(['error' => 'Endpoint not found.']);
}
?>

8. Testing the API

  1. Start Apache and MySQL:
    • Ensure that the Apache and MySQL modules are running in XAMPP.
  2. Access the API Endpoints:
    • GET All Books:
      • Navigate to http://localhost/rest-api/api/books using a browser or tools like Postman.
    • GET a Single Book:
      • Navigate to http://localhost/rest-api/api/books/{id} replacing {id} with an actual book ID.
    • POST Create a New Book:
      • Use Postman to send a POST request to http://localhost/rest-api/api/books with a JSON body: { "title": "Sample Book", "author": "Author Name", "isbn": "1234567890123", "published_at": "2023-01-01" }
    • PUT Update an Existing Book:
      • Send a PUT request to http://localhost/rest-api/api/books/{id} with the updated JSON data.
    • DELETE a Book:
      • Send a DELETE request to http://localhost/rest-api/api/books/{id}.
  3. Verify Responses:
    • Ensure that the API returns appropriate JSON responses and HTTP status codes based on the actions performed.

9. Deployment Considerations

When deploying your RESTful API to a live server, consider the following:

  • Authentication and Authorization:
    • Implement authentication mechanisms (e.g., API keys, JWT) to secure the API.
    • Restrict access to certain endpoints based on user roles or permissions.
  • CORS Configuration:
    • Adjust the Access-Control-Allow-Origin header to allow only trusted domains.
  • Input Validation and Sanitization:
    • Ensure all inputs are validated and sanitized to prevent SQL injection and other attacks.
  • Rate Limiting:
    • Implement rate limiting to prevent abuse and ensure API availability.
  • HTTPS:
    • Use HTTPS to secure data transmission between clients and the API.
  • Error Handling:
    • Provide meaningful error messages without exposing sensitive information.
  • Documentation:
    • Create comprehensive API documentation for developers to understand how to use the API effectively.
  • Versioning:
    • Implement API versioning to manage updates and maintain backward compatibility.

10. Enhancements and Best Practices

  • Pagination:
    • Implement pagination for listing endpoints to handle large datasets efficiently.
  • Filtering and Sorting:
    • Allow clients to filter and sort data based on various parameters.
  • Logging and Monitoring:
    • Implement logging for API requests and errors.
    • Use monitoring tools to track API performance and uptime.
  • Use of Frameworks:
    • Consider using PHP frameworks like Laravel or Slim to streamline API development and leverage built-in features.
  • Caching:
    • Implement caching strategies (e.g., Redis, Memcached) to improve API response times.
  • Testing:
    • Write unit and integration tests to ensure API reliability and correctness.
  • API Versioning:
    • Start with versioning (e.g., /api/v1/books) to manage future changes without breaking existing clients.
  • Rate Limiting and Throttling:
    • Implement mechanisms to control the rate of requests from clients, preventing abuse and ensuring fair usage.
Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *