Building a RESTful API Using ReactPHP and MySQL
Today we will be looking at creating a RESTful API using ReactPHP, MySQL and nikic/FastRoute. Let’s look at the API we want to build and what it can do.
Application
We are going to build the API that:
- Handles CRUD operations on a resource (we are going to use
users
) - Uses the proper HTTP verbs to make it RESTful (GET, POST, PUT, and DELETE)
- Returns JSON data
All of this is pretty standard for RESTful APIs. Feel free to switch out users for anything you want for your application (orders, products, customers, etc).
Getting Started
Here is our file structure. We won’t need many files and we’ll keep this very simple for demonstration purposes.
Installing Our Dependencies
We need to install several packages:
- react/http for running HTTP server
- friends-of-reactphp/mysql to interact with MySQL database
- nikic/fast-route to handle routing
Go to the command line in the root of the project and run the following commands:
Simple and easy. Then open composer.json
and add autoloading section. We are going to autoload classes from src
folder into App
namespace:
Now that we have our packages being installed, let’s go ahead and use them to set up our API.
Setting Up Our Server
First of all, we need a running server that will handle incoming requests. Create an empty HTTP server:
This is our entry point, our future RESTful API server. Here we have a “hello-world” HTTP server. It has one middleware $hello
which is triggered for all incoming requests and returns a plain text string ‘Hello’.
If you are not familiar with ReactPHP middleware and don’t know how they work check this article.
Let’s make sure that everything is working up to this point. Start the server. From the command line, type:
You should see the message saying that the server is up and is listening. Now that we know our application is up and running, let’s test it. Make a GET request to http://127.0.0.1:8000
.
Nice! We got back exactly what we wanted. Now, let’s wire up our database so we can start performing CRUD operations on users.
Database
Create a database and table users
with the following schema: id
, name
, and email
. Email field is unique.
Now, let’s connect to a database. Create a factory React\MySQL\Factory
. Then ask it for a lazy connection, and provide a connection string. My connection string consists of: username root
, has no password, host localhost
, and database called “test”.
The main idea of lazy connection is that internally it lazily creates the underlying database connection only on demand once the first request is invoked and it will queue all outstanding requests until the underlying connection is ready.
Creating Basic Routes
We will now create the routes to handle getting all the users and creating a new user. Both routes will be handled using the /users
path.
Getting All Users
GET /users
Create a middleware $listUsers
and pass an instance of the connection inside. Now, we are ready to execute queries. Select all users and return them as a JSON object:
Method query()
accepts a raw SQL string and returns a promise that resolves with an instance of QueryResult
. To grab resulting rows we use resultRows
property of this object. It will be an array of arrays, that represent the result of the query. Then convert them to JSON and return with an corresponding Content-type
header. I have also changed the middlware in the Server
constructor from $hello
to $listUsers
.
Check our API, make a GET request to http://127.0.0.1:8000
and you will receive an empty list. Add several users to the database and check again. Now it should return them.
MySQL query works, but the server is still a sort of “hello-world” one. It responds the same way to all incoming requests. It’s time to fix it and add routing to our application.
Create class Router
in src
folder:
It is a middleware-wrapper on top of FastRoute.
I’m not going to cover details of using FastRoute in ReactPHP project. Instead, we will focus on writing controllers and database quires. We are using here a router from one of the previous articles: Using Router With ReactPHP Http Component.
This middleware encapsulates a collection of routes and inside we check method and path of the request. $routeInfo[0]
contains the result of the matching. If the request matches one of the defined routes we execute a corresponding controller with a request object and matched params (if they were defined). Otherwise, we return 404
or 405
responses.
Then define the routes. For example, this $listUsers
middleware responds only to GET requests to path /users
.
We instantiate an instance of FastRoute\RouteCollector
with required dependencies and add our first route for GET
request. Then we pass the router to the server:
The first endpoint of our API is ready. In response to GET request to /users
path, we return a JSON representation of users.
Create a New User
POST /users
Create a new middleware (controller) $createUser
. For this endpoint, we use the same path /users
but the request method will be POST
:
Assume that we receive user data in JSON. So, we get the response body and decode it to an array:
Now we are ready to insert $user
array into users
table. Again call $db->query()
and provide a raw SQL query and an array of data to be inserted:
Once the request is done we return 201
response.
I have skipped validation here on purpose, to make examples easier to understand. In a real life, you should never trust input data.
It looks like we are performing raw queries and everything we pass inside query()
method will be placed right into the query. So, it looks like there is a room for a SQL injection. Don’t worry, when we execute a query all provided params are escaped. So, feel free to provide any values and don’t be afraid of SQL injection. OK, the endpoint is done. Let’s check it. Create a new user:
Then request all users.
We see that a new user has been stored in the database. Let’s try to add the same user one more time.
It returns 500 response. Seems like the query has failed but there is no actual error anywhere. To view the error we can add a rejection handler to our query and return an error message as a response:
Then restart the server and execute the request. Now the problem is clear. We receive a clear “bad request” response, explaining that we are trying to insert the user with a duplicated email.
Here is the complete code of $createUser
controller:
Refactoring
Our application is becoming messy. Adding more controllers will only make things worse. Let’s replace our procedural code with classes. Create folder src/Controller
. We are going to store controller classes here.
I assume that you have a root namespace
App
and autoloading in yourcomposer.json
file.
At first we extract $listUsers
controller. Move the whole logic to its own class App\Controller\ListUsers
:
Notice that a database connection is now injected into the constructor. The rest of the code stays the same. Having raw SQL queries in controllers is not a good practice. We can extract them out and create a sort of repository for users. Create class Users
in src
folder:
It encapsulates a database connection. Method all()
returns a promise that resolves with an array that contains
raw users data. Now, inside the controller, we inject an instance of Users
class instead of MySQL connection. And replace a raw SQL query with a call of Users::all()
:
Done. But there is still a room for improvement here. As it was mentioned before our API returns JSON responses. So, instead of repeating the same response building logic we can also create our own custom JsonResponse
class - a wrapper on top of React\Http\Response
. It will accept a status code and the data we want to return. JSON encoding logic and required headers will be encapsulated in this class. Create it in src
folder:
Then return an instance of JsonResponse
in the controller:
Done. The controller looks pretty simple and readable. The same way we can move $createUser
controller to its own class App\Controller\CreateUser
:
Update Users
class and a missing method create()
:
It returns a promise that resolves when a new record has been added to the database.
Status codes and response structure can be hidden behind JsonResponse
class. Let’s add some named constructors to it:
Then use them in the controller:
This makes the controller even more readable. Then we move back to the main script and replace functions with objects.
Routes for A Single Item
We’ve handled the group for routes ending in /users
. Let’s now handle the routes for when we pass in a parameter like a user’s id.
The things we’ll want to do for this route, which will end with /users/{id}
will be:
- Get a single user.
- Update a user’s info.
- Delete a user.
Get a Single User
GET /users/{id}
We start by adding a new method find(string $id)
to our Users
class:
As you can see I have added a new custom exception UserNotFoundError
. The code here is very straightforward. Make a SELECT
query and return a promise. If there is no such record in the database we throw an exception and the promise rejects. Otherwise, the promise resolves with an array of a user’s data.
Here is UserNotFoundError
class:
Before writing a controller let’s add one more custom response to JsonResponse
class. For this endpoint we definitely need a 404
response, so add a new named constructor notFound
which accepts a string:
Now we are ready to create a new controller App\Controller\ViewUser
:
Here we ask Users
object to find a user by its id and return a corresponding response. If the promise was rejected with UserNotFoundError
we return a 404
response. Otherwise, we return a JSON representation of a user. Then define a new route:
From the call to get all users, we can see id of one of our users. Let’s grab that id and test getting that single user.
We can grab one user from our API now! Let’s look at updating that user’s name.
Update a User’s Name
PUT /users/{id}
Our users have only three possible fields: id
, email
, and name
. We will allow changes only for names. Update Users
and add a new method update()
:
The method accepts an id of the user and a new name. We don’t check for affectedRows
property of the QueryResult
object because in UPDATE
request we cannot detect whether there is no record with such id, or there is no need in updating a field value. So, we need to find a user first. And if there is such record in the database we perform an update. The resulting promise rejects with UserNotFoundError
or resolves on successful update.
Then create a new controller App\Controller\UpdateUser
:
Here we extract the name
field from the received request body. If it is not present or is empty we return a bad request. Then we try to update a user. If it has been successfully updated and we return 204
response. If the promise rejects we return 404
response. I have already updated JsonResponse
class with a new static constructor for 204
status code:
Add a new route:
Make a PUT request to check that everything works as expected.
We have received 204 status code and a record in the database has changed.
Deleting a User
DELETE /users/{id}
The last endpoint in our tutorial is responsible for deleting a user. Create a new method delete()
in Users
class:
Here we check affectedRows
property of the QueryResult
object to detect whether a user has been deleted or not.
App\Controller\DeleteUser
controller looks the following:
As always define a new route:
Now when we send a request to our API using DELETE method with the proper user’s id, we’ll delete this user. For example, let’s delete our first user with id 1
.
Conclusion
We now have the means to handle CRUD on a specific resource through our own API. Using the techniques above should be a good foundation to move into building larger and more robust APIs.
This has been a quick look at creating RESTful API with ReactPHP and MySQL. There are many more things you can do. For example, you can add authentication and create validation with better error messages.
Further Reading: in the next articles we will cover different types of authentication that can be used to protect RESTful API endpoints:
You can find examples from this article on GitHub.
This article is a part of the ReactPHP Series.