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 to public 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 an ApiResponse 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-7 ResponseInterface 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:

  1. Visit the Settings->REST API menu, and change the authentication schema to "API Key / Secret".
  2. Visit the Users->Create New User menu and create a user.
  3. 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.