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
- Install XAMPP:
- Download from XAMPP Official Website.
- Follow the installation wizard and install it in the default directory.
- Start Apache and MySQL:
- Open the XAMPP Control Panel.
- Start the Apache and MySQL modules.
- Install Composer (Optional):
- If you plan to use PHP packages or libraries, install Composer from Composer Official Website.
2. Creating the Database
- Access phpMyAdmin:
- Navigate to
http://localhost/phpmyadmin/
in your browser.
- Navigate to
- 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”.
- Create a
books
Table:- Select the
rest_api
database. - Click on “New” to create a table.
- Define the table with the following fields:
- Click “Save”.
- Select the
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
- Start Apache and MySQL:
- Ensure that the Apache and MySQL modules are running in XAMPP.
- Access the API Endpoints:
- GET All Books:
- Navigate to
http://localhost/rest-api/api/books
using a browser or tools like Postman.
- Navigate to
- GET a Single Book:
- Navigate to
http://localhost/rest-api/api/books/{id}
replacing{id}
with an actual book ID.
- Navigate to
- 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" }
- Use Postman to send a POST request to
- PUT Update an Existing Book:
- Send a PUT request to
http://localhost/rest-api/api/books/{id}
with the updated JSON data.
- Send a PUT request to
- DELETE a Book:
- Send a DELETE request to
http://localhost/rest-api/api/books/{id}
.
- Send a DELETE request to
- GET All Books:
- 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.
- Adjust the
- 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.
- Start with versioning (e.g.,
- Rate Limiting and Throttling:
- Implement mechanisms to control the rate of requests from clients, preventing abuse and ensuring fair usage.