REST APIs Made Easy
Apex provides a very simple and straight forward interface to develop secure and efficient JSON based REST APIs. Learning by example, this guide will step you through creating a simple REST API.
Getting Started
First, ensure you have the rest-api
package installed with the following command:
./apex install rest-api
Once installed, if you look in the ~/boot/routes.yml file you will notice a new entry has been added of:
routes:
api: RestApi
You will also notice a new HTTP controller located at ~/src/HttpControllers/RestApi. This means all HTTP requests sent to any path starting with /api/ will be handled by the RestApi
HTTP controller.
Next, if you don't already have a package to develop with, create one with the command:
./apex package create demo
Create API Endpoint
Create a new API endpoint with the command:
./apex opus api-endpoint Demo/Api/Prices/Add
This simply creates a new file at ~/src/Demo/Api/Prices/Add.php, and nothing more. If you would prefer not to use the command line, you may copy this class over for each new API endpoint.
Open the file at ~/src/Demo/Api/Prices/Add.php and replace it with the following contents:
<?php
declare(strict_types = 1);
namespace App\Demo\Api\Prices;
use App\RestApi\Helpers\ApiRequest;
use App\RestApi\Models\{ApiResponse, ApiDoc, ApiParam, ApiReturnVar};
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use redis;
/**
* Add API endpoint
*/
Class Add extends ApiRequest
{
#[Inject(redis::class)]
private redis $redis;
/**
* The auth level required to access this endpoint.
* Supported values are: public, user, admin
*/
public string $acl_required = 'public';
/**
* Specify description of the endpoint here, which will be extracted upon documentation generation.
*/
public function post(ServerRequestInterface $request, RequestHandlerInterface $app):ApiResponse
{
// Check required
if (!$this->checkRequired('name', 'price')) {
return $this->getResponse();
}
// Add the price
$this->redis->hset('prices', $app->post('name'), $app->post('price'));
// Set response data
$data = [
'name' => $app->post('name'),
'price' => $app->post('price')
];
// Return
return new ApiResponse(200, $data, "Added new price");
}
}
The following changes have been made from the default class:
- Added
redis
to the use declarations. - Added the necessary
#[Inject(redis::class)]
attribute for injection. - Changed the value of the
$acl_required
property topublic
meaning no authentication is required for this endpoint. - Filled out the
post()
method to check and ensure the form fields 'name' and 'price' were posted, then adds them to redis and returns anApiResponse
instance.
The API endpoint is located at http://127.0.0.1/api/demo/prices/add. When a new HTTP request is sent to /api/ it's forwarded to the RestApi
HTTP controller, which uses the first segment as the package alias, and all other segments are mapped to the PHP class relative to the /Api/ sub-directory of the package.
You will notice the above PHP class is very similar to a PSR-15 middleware class with two major differences:
- It returns an
ApiResponse
object instead of a PSR-7ResponseInterface
object. - The methods are named the same as the HTTP verbs of the endpoint, so you can have method names such as
post()
,get()
,put()
,delete()
, and so on.
Test API Endpoint
Open a blank test.php file within the Apex installation directory, and enter the following contents:
<?php
use Apex\App\App;
use Apex\Svc\HttpClient;
use Nyholm\Psr7\Request;
// Boot apex
require_once('./vendor/autoload.php');
$app = new App();
// Get http client
$cntr = $app->getContainer();
$http = $cntr->get(HttpClient::class);
// Set post vars
$post = [
'name' => 'laptop',
'price' => 1195
];
// Send http request
$req = new Request('POST', 'http://127.0.0.1/api/demo/prices/add', ['Content-type' => 'application/x-www-form-urlencoded'], http_build_query($post));
$res = $http->sendRequest($req);
// Decode json
$json = json_decode($res->getBody()->getContents(), true);
print_r($json);
Run the script in terminal, and assuming everything is correctly in place, you will get the following response:
[data-line=-1]
Array
(
[status] => ok
[version] => 1
[message] => Added new price
[data] => Array
(
[name] => laptop
[price] => 1195
)
)
All API responses are formatted in the same manner. They will contain a status
element of either "ok" or "error", a message
element that is the third argument passed to the ApiResponse
constructor and intended as an internal message for developers. Plus it will contain the data
element which is the array passed as the second argument to the ApiResponse
constructor.
Dynamic Path Parameters
Create another endpoint with the command:
./apex opus api-endpoint Demo/Api/Prices/Lookup --route api/demo/prices/lookup/:item_name
This will create the new PHP class as usual, but since the --route
flag was also defined will add the route to both, the ~/boot/routes.yml file and also the ~/etc/Demo/registry.yml file. The latter will ensure the route is automatically created when the package is installed on another Apex system. You will also notice the colon in front of /:item_name
within the route, which signifies a dynamic path parameter.
Next, open the file at ~/src/Demo/Api/Prices/Lookup.php and replace its contents with the following:
<?php
declare(strict_types = 1);
namespace App\Demo\Api\Prices;
use App\RestApi\Helpers\ApiRequest;
use App\RestApi\Models\{ApiResponse, ApiDoc, ApiParam, ApiReturnVar};
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use redis;
/**
* Lookup API endpoint
*/
Class Lookup extends ApiRequest
{
#[Inject(redis::class)]
private redis $redis;
/**
* The auth level required to access this endpoint.
* Supported values are: public, user, admin
*/
public string $acl_required = 'public';
/**
* Specify description of the endpoint here, which will be extracted upon documentation generation.
*/
public function get(ServerRequestInterface $request, RequestHandlerInterface $app):ApiResponse
{
// Get item name
$name = $app->pathParam('item_name');
// Check redis for price
if (!$price = $this->redis->hget('prices', $name)) {
return new ApiResponse(404, [], "No listing with the name, $name");
}
// Return
return new ApiResponse(200, ['price' => $price]);
}
}
Same as the previous endpoint, we added redis
to the use declarations plus the injection attribute, and changed the value of the $acl_required
property to public
. However, instead of a post()
method a get()
method was developed out, meaning this endpoint will only work with HTTP GET requests, and not POST requests.
The get()
method simply ascertains the dynamic path parameter item_name
that was defined in the route, then checks redis whether or not a price exists for that item. If not, it will return a 404 error, and if yes will return a 200 response with the corresponding price.
Try it yourself, and open your browser to http://127.0.0.1/api/demo/prices/lookup/laptop, and the one price previously added in the test script above will be given. Then try something other than laptop
, and the appropriate 404 error response will be given instead.
Authentication
Let's change the /api/demo/prices/add endpoint, and make it require authentication. Login to the administration panel, and complete the following steps:
- Visit the Settings->REST API menu, and change the authentication schema to "API Key / Secret".
- Visit the Users->Create New User menu and create a user.
- Visit the Users->Manage User menu, and manage the user you just created. Within the "API Keys" tab click on the submit button to generate a new API key. Upon the page refreshing, view the "API Keys' tab again and copy down the newly generated API key and secret.
Next, open the file at ~/src/Demo/Api/Prices/Add.php and change the value of the $acl_required
property to user
:
[data-line=33]
`public string $acl_required = 'user';
Save the file, and upon running the test script again in terminal, it will throw a 401 Unauthorized request response. Open the test script in a text editor, and change its contents to the following:
<?php
use Apex\App\App;
use Apex\Svc\HttpClient;
use Nyholm\Psr7\Request;
// Boot apex
require_once('./vendor/autoload.php');
$app = new App();
// Get http client
$cntr = $app->getContainer();
$http = $cntr->get(HttpClient::class);
// Set post vars
$post = [
'name' => 'car',
'price' => 44995
];
// Set headers
$headers = [
'Content-type' => 'application/x-www-form-urlencoded',
'API-Key' => '7cea3ab28f92dd3450640cbb',
'API-Secret' => '730e43f6ae398e2f077d045a32b46cdc81c9'
];
// Send http request
$req = new Request('POST', 'http://127.0.0.1/api/demo/prices/add', $headers, http_build_query($post));
$res = $http->sendRequest($req);
// Decode json
$json = json_decode($res->getBody()->getContents(), true);
print_r($json);
Naturally, change the API key and secret within the $headers
array to your own. Run the test script again in terminal, and the request will now go through fine. For full details on authentication, please visit the Authentication Schemas page of the REST API documentation.
Moving Forward
You should now have a solid understanding of how to efficiently develop out proper and secure REST APIs using Apex. To continue, check out the various other available guides or the full developer documentation. If you ever need any assistance with Apex, you can always ask on the /r/apexpl sub Reddit.