Developing a RESTful PHP API on Ubuntu 20.04
Understanding APIs and Their Use Cases
An Application Programming Interface (API) acts as a middleware component, enabling communication between different software systems via a REST (Representational State Transfer) protocol. For example, when designing a mobile app that relies on cloud storage, a PHP-based API can serve as a bridge between an Android client and a remote MySQL database.
Another common scenario involves offering your application’s functionality to external users through public endpoints. Well-known platforms like Google, Twitter, Facebook utilize public APIs for this purpose. Instead of navigating through a graphical user interface (GUI), users can directly interact with your application via specific URLs that handle data retrieval and submission.
Why Building an API Enhances Integration
Incorporating an API into your software enhances flexibility, allowing users to connect with your services through various applications. Moreover, when API responses are formatted in JSON (JavaScript Object Notation), compatibility spans across mobile apps, desktops, tablets, and even IoT devices—without requiring backend modifications.
This guide walks you through creating a RESTful JSON API in PHP for a fictional online shop, running on Ubuntu 20.04. Once completed, you’ll be able to access product data from the database without interacting with a traditional user interface.
Requirements
- An Ubuntu 20.04 environment
- A user account with sudo privileges
- A functioning LAMP stack (either MySQL or MariaDB)
Building the Sample Database
Begin by logging into your server using SSH and launching the MySQL shell:
$ sudo mysql -u root -p
After entering your root password and seeing the mysql>
prompt, execute the following command to create a new database named store_api
:
mysql> CREATE DATABASE store_api;
Next, generate a dedicated MySQL user for your API to use during database access:
mysql> CREATE USER 'api_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'EXAMPLE_PASSWORD';
mysql> GRANT ALL PRIVILEGES ON store_api.* TO 'api_user'@'localhost';
mysql> FLUSH PRIVILEGES;
For those using MariaDB instead of MySQL, use the following syntax to grant access:
MariaDB> GRANT ALL PRIVILEGES on store_api.* TO 'api_user'@'localhost' identified by 'EXAMPLE_PASSWORD';
Switch the current session to the newly created database:
mysql> USE store_api;
Defining the Products Table
Now, create a table named products
to represent the items available in your online store. This will allow your API to fetch product details, which users would normally only see through a frontend interface.
mysql> CREATE TABLE products (
product_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(50),
cost_price DOUBLE,
retail_price DOUBLE
) ENGINE = InnoDB;
Insert sample entries into the table to simulate a real inventory:
mysql> INSERT INTO products (product_name, cost_price, retail_price) VALUES ('LEATHER JACKET', '89.23', '99.95');
mysql> INSERT INTO products (product_name, cost_price, retail_price) VALUES ('SILVER COAT', '44.00', '60.00');
mysql> INSERT INTO products (product_name, cost_price, retail_price) VALUES ('REXI BELT', '14.49', '18.85');
mysql> INSERT INTO products (product_name, cost_price, retail_price) VALUES ('SUEDE SHOE', '24.00', '36.00');
mysql> INSERT INTO products (product_name, cost_price, retail_price) VALUES ('WOOLEN SWEATER', '14.45', '18.00');
Run a SELECT query to confirm that your entries have been correctly saved:
mysql> SELECT
product_id,
product_name,
cost_price,
retail_price
FROM products;
MySQL should return a list similar to this:
+------------+----------------+------------+--------------+ | product_id | product_name | cost_price | retail_price | +------------+----------------+------------+--------------+ | 1 | LEATHER JACKET | 89.23 | 99.95 | | 2 | SILVER COAT | 44 | 60 | | 3 | REXI BELT | 14.49 | 18.85 | | 4 | SUEDE SHOE | 24 | 36 | | 5 | WOOLEN SWEATER | 14.45 | 18 | +------------+----------------+------------+--------------+ 5 rows in set (0.00 sec)
Exit the MySQL session once your setup is complete:
mysql> QUIT;
Setting Up Apache ModRewrite for API Routing
To ensure your API endpoints function correctly, it’s essential to configure Apache’s URL rewriting capabilities. Start by activating the mod_rewrite
module on your server:
$ sudo a2enmod rewrite
Next, open the main Apache configuration file using nano:
$ sudo nano /etc/apache2/apache2.conf
Locate the directive block associated with /var/www/
and make the following adjustment:
... <Directory /var/www/> Options Indexes FollowSymLinks AllowOverride None Require all granted </Directory> ...
Replace the AllowOverride None
line with AllowOverride All
so that Apache can respect the settings inside .htaccess
files:
... <Directory /var/www/> Options Indexes FollowSymLinks AllowOverride All Require all granted </Directory> ...
This change allows the .htaccess
files to override the default server behavior. After making this adjustment, restart Apache to apply the changes:
$ sudo systemctl restart apache2
Creating a .htaccess File for URL Rewriting
Now that mod_rewrite is enabled, create a base folder for your API project. In this example, a versioning approach is used, starting with version 1 as the directory name:
$ sudo mkdir -p /var/www/html/api/v1
Within that folder, create a new .htaccess
file:
$ sudo nano /var/www/html/api/v1/.htaccess
Insert the following lines into the file:
RewriteEngine On
RewriteBase /api/v1
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule (.*)$ index.php?request=$1 [QSA,NC,L]
Explanation of the .htaccess Directives
- RewriteEngine On: This enables Apache’s URL rewriting engine.
- RewriteBase /api/v1: Defines the base path for all subsequent rewrite rules.
- RewriteCond %{REQUEST_FILENAME} !-f: Skips rewrite rules if the requested file exists.
- RewriteCond %{REQUEST_FILENAME} !-d: Skips if the requested path is a directory.
- RewriteRule (.*)$ index.php?request=$1 [QSA,NC,L]: Redirects any remaining requests to
index.php
with the request URI passed as a parameter.
For example, a request to http://localhost/api/v1/products/1
will be internally rewritten to http://localhost/api/v1/index.php?request=products/1
.
Creating the Main index.php Router
The next step is to set up the main index.php
file that will handle all incoming API requests. This file acts as the central router, directing traffic to the appropriate functionality based on the URL and request method.
Use the following command to create and open the file:
$ sudo nano /var/www/html/api/v1/index.php
Insert the following PHP code into the file:
<?php
header("Content-type:application/json");
function load_class($class) {
require_once $class . '.php';
}
spl_autoload_register('load_class');
$http_verb = $_SERVER['REQUEST_METHOD'];
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
foreach($_GET as $key => $value) {
$params[$key] = $value;
}
}
$request = explode('/', $_REQUEST['request']);
$resource = $request[0];
if (isset($request[1])) {
$resource_id = $request[1];
} else {
$resource_id = '';
}
if ($resource == 'products') {
$request = new Products;
}
if ($http_verb == 'GET') {
if (!empty($resource_id)) {
$response = $request->read($resource_id);
} else {
$response = $request->read($resource_id, $params);
}
echo $response;
}
Understanding the index.php Code
- header(“Content-type:application/json”): This line ensures that all output from the API is sent in JSON format, which is ideal for web and mobile clients.
- load_class function: This custom function simplifies class loading by using PHP’s autoload feature, preventing the need for multiple
require
statements. - spl_autoload_register: Registers the autoloader so that class files are loaded dynamically when needed.
- Handling GET Requests: If the request method is GET, any incoming GET parameters are collected and stored in the
$params
array. - Parsing the Request: The URL path provided to the API is split into its components. The first part identifies the resource type (e.g.,
products
), and the second, if present, is the specific resource ID. - Resource Mapping: When the API identifies a
products
request, it initializes a new instance of theProducts
class to handle the logic. - GET Execution Flow: If a resource ID is provided, the system retrieves a specific product using the
read
method. Otherwise, it performs a more general query, potentially using URL filters supplied via GET.
For instance, accessing http://localhost/api/v1/products/1
will instruct the API to fetch the item with ID 1. On the other hand, a call to http://localhost/api/v1/products?type=shoes
allows for parameter-based filtering instead.
Building the Products Class for Handling Requests
To respond to API calls targeting the products
endpoint—such as http://localhost/api/v1/products
or http://localhost/api/v1/products/1
—you need to create a class called Products
.
Create the file using the following command:
$ sudo nano /var/www/html/api/v1/Products.php
Add this PHP code to define the class:
<?php
class Products
{
public function read($resource_id, $params = '')
{
try {
$db_name = 'store_api';
$db_user = 'api_user';
$db_password = 'EXAMPLE_PASSWORD';
$db_host = 'localhost';
$pdo = new PDO('mysql:host=' . $db_host . '; dbname=' . $db_name, $db_user, $db_password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$data = [];
$sql = "select * from products";
if (!empty($resource_id)) {
$sql .= " where product_id = :product_id";
$data['product_id'] = $resource_id;
} else {
$filter = '';
if (isset($params['product_name'])) {
$filter .= " and product_name = :product_name";
$data['product_name'] = $params['product_name'];
}
$sql .= " where product_id > 0 $filter";
}
$stmt = $pdo->prepare($sql);
$stmt->execute($data);
$products = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$products[] = $row;
}
$response = [];
$response['data'] = $products;
if (!empty($resource_id)) {
$response['data'] = array_shift($response['data']);
}
return json_encode($response, JSON_PRETTY_PRINT);
} catch (PDOException $ex) {
$error = [];
$error['message'] = $ex->getMessage();
return $error;
}
}
}
Understanding the Products Class Implementation
- Class Structure: The
Products
class is invoked from theindex.php
file whenever a user targets theproducts
endpoint. - read() Method: The core logic resides inside the
read
method, which accepts a resource ID and optional parameters. This method is what executes and responds to incoming GET requests. - Database Connection: PDO is used to connect to the MySQL database securely. The database credentials match those previously configured.
- Query Logic: If a specific product ID is provided, the SQL query fetches only that product. Otherwise, a filter can be applied using query parameters, such as
product_name
, to return a refined list of results. - Safe Execution: Named parameters are used to prevent SQL injection. The query is prepared and executed using
$stmt->execute()
. - JSON Response: The output is encoded into a neatly formatted JSON object. If only one item is fetched, it’s returned directly instead of in an array.
- Error Handling: If the database operation fails, the error message is captured and returned in JSON format.
Testing the PHP API with curl
Once everything is in place, test your API using the curl
command to confirm that the endpoint behaves as expected.
View All Products
$ curl localhost/api/v1/products
Sample output:
{ "data": [ { "product_id": 1, "product_name": "LEATHER JACKET", "cost_price": 89.23, "retail_price": 99.95 }, { "product_id": 2, "product_name": "SILVER COAT", "cost_price": 44, "retail_price": 60 }, { "product_id": 3, "product_name": "REXI BELT", "cost_price": 14.49, "retail_price": 18.85 }, { "product_id": 4, "product_name": "SUEDE SHOE", "cost_price": 24, "retail_price": 36 }, { "product_id": 5, "product_name": "WOOLEN SWEATER", "cost_price": 14.45, "retail_price": 18 } ] }
Retrieve a Single Product by ID
Product ID 1:
$ curl localhost/api/v1/products/1
{ "data": { "product_id": 1, "product_name": "LEATHER JACKET", "cost_price": 89.23, "retail_price": 99.95 } }
Product ID 2:
$ curl localhost/api/v1/products/2
{ "data": { "product_id": 2, "product_name": "SILVER COAT", "cost_price": 44, "retail_price": 60 } }
Filter by Product Name
Retrieve the “SUEDE SHOE” entry:
$ curl localhost/api/v1/products?product_name=SUEDE%20SHOE
{ "data": [ { "product_id": 4, "product_name": "SUEDE SHOE", "cost_price": 24, "retail_price": 36 } ] }
Final Thoughts
You’ve now successfully implemented a RESTful API using PHP and MySQL on Ubuntu 20.04. The API delivers data in JSON format and is fully capable of responding to GET requests. You can further expand this setup by adding support for additional HTTP methods like POST, PUT, and DELETE, tailoring the logic to meet your application’s needs.