As part of my Borum Jot project, I made a Web API for all my front-end platforms to create, retrieve, update, and delete (CRUD) data on the database. In this article, I'll discuss what exactly an API is and how I made my own Web API.
There are two main kinds of APIs.
One is a software and its documentation, and this is used very liberally. For example, the old JavaScript library jQuery has a documentation so developers know how to use it. This documentation web site is referred to as the jQuery API. Other examples are documentations for programming languages, frameworks, or even ecosystems. It is usually specified in the URL, even if it's not directly on the page.
Thanks to @mankms on Twitter, I learned that a Web API refers to sets of pages to which clients make network requests, and they return back data, possibly querying a database, in a specific structure. I like to think of a Web API as part of a filtration system:
This concept is called information hiding, and it is important for simplicity and security. But before I could begin hiding information, I had to obtain information. I began by choosing a language.
Choosing a Language and Content Type
I used PHP, a back-end language, for my API. A back-end language sits on the server and accesses data.
I used JSON, or JavaScript Object Notation, for the content type. It is used to represent data in nested objects. In my API, I sent JSON that looked like this:
{
"statusCode": 200,
"data": [
{
"id": "10",
"title": "Go to Grocery Store",
"body": "Go to this grocery store",
"user_id": "115",
"completed": "1",
"time_updated": "1038829292",
"parent_id": "0",
"priority": "0",
"source": "task"
}
]
}
Once the client receives the above response and parses the JSON, it can retrieve the value of any property, such as statusCode
.
Host Configuration and File Structure
I now knew I was writing in PHP to make a Web API that sends JSON to a client (the front-end). Surprisingly, where I hosted was very important at this point. I deployed on Vercel, a hosting platform. Although Vercel is designed for JavaScript and React-type projects, the vercel-php runtime allows developers to host backend PHP projects on a secure domain (as opposed to directly on the domain registrar, which may require configuring GitHub, paying for an SSL certificate, etc.).
Because I was deploying on Vercel, I needed to store all the endpoints in the api directory and configure the now.json
file. The now.json
file tells vercel-php which files were serverless functions. These serverless functions served as the API endpoints that sent the response data in JSON format.
Vercel Function Logs
In addition, I've seen APIs specify their version (e.g. v1). So far, this was my file structure (note the composer.json is for including PHP libraries)
api/
v1/
?
now.json
composer.json
What would go inside the v1 subdirectory?
Writing my First Endpoint
Finally, I could begin writing the code (the fun part!).
In its simplest form, a Web API is one PHP file that returns the JSON, such as one of the examples above, on every request.
<?php
header("HTTP/1.1 200 OK");
header("Content-Type: application/json; charset=UTF-8");
echo json_encode(["english" => "Hello world!", "french" => "Bonjour!", "german" => "Guten tag!"];
?>
Notice the header function calls, establishing the server response headers. The 200 OK
is an HTTP Status Code that tells the client that everything ran okay and it can now access the data. The json_encode
turns a PHP Associative Array into JSON format. The API deploys through Vercel and has the following as its now.json
file:
{
"functions": {
"api/v1/*.php": {
"runtime": "vercel-php@0.3.1"
},
},
"routes": [
{ "src": "/v1/greeting", "dest": "/v1/greeting.php" }
]
}
The functions
object specifies the directories that have API endpoints and will always have the same runtime property. The routes
array contains paths at the location of dest
that rewrite the url to src
. In the above example, /v1/greeting.php
simply becomes /v1/greeting
.
Helper Classes and Added Complexity
The second level of complexity was to interact with the database. I created a separate folder called include
and put it in the api
directory in case I wanted to have a v2
. Remember, putting everything in one file still makes it an API, but I put database handling in a separate class to make my code DRY and modular. When I have more than one response object (such as a GET and POST request to the same endpoint, or multiple endpoints), I could call code I had already written. My modified file structure looked like this:
api/
include/
Config.php
SimpleRest.php
DBHandler.php
v1/
greeting.php
now.json
composer.json
I copied the SimpleRest
code from another site, expanding to fit to my API.
For every response from this point, I had only to call SimpleRest::setHttpHeaders(200)
or another status code, replacing multiple header()
calls with one method call of my own class.
Database Interaction
First and foremost is a Config.php file for database interaction. I used the mysqli_connect()
call to connect with my database credentials to the MySQL database I host on GoDaddy. Next, I stored the PHP Connection object inside an instance variable named $conn
in the DBHandler
class in the constructor.
For every database-querying function, I wrote an instance method in the DBHandler class. Once my database, app, and API got larger, I expanded this into its own folder and namespace, but for now, I could keep everything in one class.
For instance, I wrote the createNewUser()
method for a POST request to the register.php
endpoint. Below is the code of this method.
public function createNewUser($firstname, $lastname, $email, $password) {
if ($this->newUserValid($email)) {
$query = "INSERT INTO firstborumdatabase.users
(first_name, last_name, email, pass, registration_date)
VALUES ('$firstname', '$lastname', '$email', SHA2('$password', 512), NOW())
";
$newBorumUserResult = $this->executeQuery($query);
if (mysqli_affected_rows($dbc) == 1) { # Query ran okay
$accountId = $this->executeQuery("SELECT id FROM firstborumdatabase.users ORDER BY registration_date DESC LIMIT 1");
$accountId = mysqli_fetch_array($accountId, MYSQLI_BOTH);
$apikey = $this->generateApiKey();
// If the generated api key is taken, keep generating until a unique one is found
while ($this->apiKeyExistsInDatabase($apikey) != true) {
$apikey = $this->generateApiKey();
}
// Insert the newly created Borum user into the Borum Jot `users` table
$newBorumJotUserResult = $this->executeQuery("INSERT INTO users (borum_user_id, time_created, api_key) VALUES ($accountId, NOW(), '$apikey')");
if (mysqli_affected_rows($dbc) == 1) { # Query ran okay
return [
"ok" => true,
"statusCode" => 200
];
}
}
return [
"error" => [
"message" => "The user could not be validated at this time"
],
"statusCode" => 500
];
} else {
return [
"error" => [
"message" => "User with that email already exists on Borum"
],
"statusCode" => 500
];
}
}
And in the endpoint file, I would call it and my helper methods inside of one case of a switch statement that checks the request method:
case 'POST':
SimpleRest::handlePostParameterValidation("name");
$newnoteres = $handler->createNote($_POST['name']);
SimpleRest::setHttpHeaders($newnoteres["statusCode"]);
echo json_encode($newnoteres);
break;
The $handler
variable instantiates a new DBHandler
object (or a subclass of that). I put a statusCode
property in each JSON response so I could easily set the status code header.
And that's what I did for every new response that I needed to create - I made a new method that queried the database in a DBHandler
class. If you know how to use PHP with MySQL, and you followed everything described above, you're ready to make your own Web API!
Don't I have to document it? What about unit tests? Stay tuned for Part 2, where I'll be covering all of this and more!
Top comments (0)