Right (and easy) setup for REST API without API Tools Admin?

Hi Community,

I’m new to Laminas, and after reading the documentation many times, I’ve not reached a solution yet.

To give you some context information, the project I’m working on it’s an existing Laminas application, dependencies are updated to the latest versions.

This application has some existing APIs, where Controllers are extending AbstractRestfulController.

We have tried to install api-tools basically for API Versioning, and maybe to use API Content validation/negotiation.

Now it seems to me that the whole ApiTools set, is not intented to be used without the Api Admin tool, because in the documentation, I cannot find any “code only” guide, or code underneath the Admin tool screenshots.

I’ve got some doubts about the right path to follow now, and maybe we can understand to proceed.

The main question that I’d like to ask you is: is Api Tools really required in order to configure a Rest Api?
If yes, how we are supposed to use it, in a project where the admin tool cannot be added?
The setup to obtain a working API seems a bit too complex, consider that we just need to add Rest controllers (with methods mapped to each Http method: GET, POST, PUT, PATCH, DELETE), decide which methods to use in each controller, and most of the time, return simple JSONs as response.

It would be nice to use Validators/Filters on the Request.

I’m not able to reach a conclusive point by myself, can we try to find a solutions?

Thanks a lot.

@funder7 Hello and welcome to our forums! :smiley:


@ezkimo can you help here?

1 Like

Hey @funder7,

welcome to the forums. The Laminas API tools can be executed in development and in production mode. If the API Tools are in development mode, following modules are loaded additionally via development.config.php in your config folder.

<?php

return [
    // Development time modules
    'modules' => [
        'Laminas\DeveloperTools',
        'Laminas\ApiTools\Admin',
    ],
    // development time configuration globbing
    'module_listener_options' => [
        'config_glob_paths' => [realpath(__DIR__) . '/autoload/{,*.}{global,local}-development.php'],
        'config_cache_enabled' => false,
        'module_map_cache_enabled' => false,
    ],
];

The development mode can be activated with a composer command.

$ composer development-enable

Conversely, this means that the both modules mentioned are not loaded in production and therefore do not have to be present in the modules folder. I would always recommend building up a detailed REST API with the admin UI and deploy it without development settings. Basically this is a code only solution.

The API Tools map HTTP methods to the respective listener methods. If a method is not implemented, an APIProblemResponse will be returned. Normally you 'll see a HTTP 501 Not Implemented response in that case. Content negotiation / validation works only with POST, PUT and PATCH. You can add separate filters and validators for each HTTP method specifically.

Note: Content Validation Request Methods
Content Validation currently only works for POST, PATCH, and PUT requests. If you need to validate query string parameters, you will need to write your own logic for those tasks.

Taken from the API Tools documentation.

Hope this helped. If you have further questions, just contact me in this thread. I will try to help you as good as I can.

1 Like

Hi @ezkimo, thanks for your response.

I’m asking here because I’ve read the documentation many times… I think that I’ve tried everything, but for some reason, I wasn’t able to setup RestController(s) and their routing.

Not considering the Admin tools UI and the various dependencies for a moment, how the module configuration should be structured in order to instantiate a new RestController and make it available on a certain route?

I’d expect that definining the route path, and the controller class name to which is linked, in union with the logic in AbstractRestfulController, would make every http method available.
Instead I had two outcomes:

  1. Errors about the controller class not being found
  2. Controller found, but always receiving 405 Method not allowed, even after overriding the called method in my controller class.

But the main point (at least for me), is to understand clearly what is the basic module configuration to create a new RestController.

Thanks

Okay, obviously there are some things we need to clarify in terms of API tools. I’ll take some time and try to express everything as understandable as possible.

1. Controllers
The Laminas API Tools are event driven. Sure, there is a basic rest controller. But this one just handles the different resources by different routes and HTTP methods. The Laminas\ApiTools\Rest\RestController extends the Laminas\Mvc\Controller\AbstractRestfulController. In normal use cases you don 't need another controller for REST API Services, because this controller calls your specific resource.

2. Resources
A resource is an event listener that will be triggered by the rest controller. A resource contains all the methods you need when acting as a rest service: create, delete, deleteList, fetch, fetchAll, patch, patchList, replaceList and update. If you don 't implement any of this methods in your resource, the response is 405.

For a better understanding I suggest installing the Laminas API Tools skelleton application. Beside that I 'll try to give you a minimum example of a rest config for you. Just give me a few moments.

1 Like

Hi @ezkimo,

Thanks again for the information, during these days I did a little more experience on Laminas, and finally found a simple solution to the problem.

In detail, the requirements where:

  • MVC as base architecture
  • Controllers linked to REST URLs, for example:
    • fetch all orders: [GET] /api/orders
    • get a single order: [GET] /api/orders/{id} + request
    • update an order: [PATCH] /api/orders/{id} + request
    • and so on…
  • Some services, not necessarily linked to entities, and exposed in the same manner as the previous point:
  • e.g.: [POST] /api/login + { "username": "abc", "password": "xyz123" } ← login is not linked to a physical resource (or model/entity), instead, it’s a “virtual” resource capable of returning different response codes:
  • 404 resource / endpoint not found
  • 500 server error
  • 400 bad request (e.g. a missing field in the request: { "username": "abc"})
  • 405 method not allowed
  • 200 OK

The last point was the one that made me think about API Tools not being the right tool for the job.
Probably I didn’t understand the documentation properly… but it seemed too complex to complete this setup.

My solution is probably far from perfection, but requires very little config, and does the job for the moment:

  1. Install "laminas/laminas-diactoros" to obtain the JsonResponse object (I’ll explain later)
  2. Create a BaseRestController class, by extending \Laminas\Mvc\Controller\AbstractRestfulController
  3. In this base controller, add the following override:
    public function onDispatch(MvcEvent $e)
    {
        $originalResult = parent::onDispatch($e);

        if ($originalResult instanceof JsonResponse) {
            $jsonRespToModel = new JsonModel($originalResult->getPayload());
            $e->setResult($jsonRespToModel);
            $e->getResponse()->setStatusCode($originalResult->getStatusCode());

            return $jsonRespToModel;
        }

        return $originalResult;
    }
  1. Now you’re ready to create the first real controller, let’s call it LoginController:
class LoginController extends BaseRestController
{
    /**
     * [POST] /login
     *
     * @param array|mixed $data request data
     *
     * @return JsonResponse
     */
    public function create($data): JsonResponse
    {
           if(true) {
               $httpStatus = 200;
            }
            else { 
               $httpStatus = 500;
           }

           return new JsonResponse(['resp_body' => 'some data here'], $httpStatus);
     }
}

  1. Now it’s time to define the routing, nothing fancy:
'api.v1' => [
                'type'         => Segment::class,
                'options'      => [
                    'route'       => '/api/:controller[/:id]',
                    'constraints' => [
                        'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
                        'id'         => '[a-zA-Z][a-zA-Z0-9_-]*',
                    ]
                ],
            ],

:controller is mandatory, basically an alias linked to the controller (in this example we need to call /api/login → the alias will be called login.
[:id] → optional parameter, not used in this call.

Conclusion
The setup is the simplest solution that I’ve reached, in order to return a Json reponse, and setting the HTTP status.
In fact, JsonModel doesn’t allow you to set the http status, from my point of view (I’m a novice with laminas), JsonModel looks more like a Json that is ready to be included in a View. While the JsonResponse class offers the whole set of features of a Response: status code, body, headers, etc…

I’d like to understand if API Tools can do something similar, and if yes, can you pleas show me a configuration that will work like the above example? (/login endpoint)

Thank you!

Sorry, I 'm a bit late …

Your setup is quite easy and creates a simple REST controller that will work. But this implementation does not use a single advantage of the API tools. No validation, no content matching, no automatic handling of request and return headers. You literally do everything by hand here. There is absolutely no need for that.

Here is a small example of how you install and configure only the code base and not the admin UI.

First install the API Tools meta module:

$ composer require laminas-api-tools/api-tools
$ composer update

After the installation edit your config/application.config.php or config/modules.config.php file. Add the module under the module key.

'modules' => [
    /* ... */
    'Laminas\ApiTools',
],

From now on you can programmatically define your resources and api endpoints. Even some basic authentication listeners are implemented in the base config of laminas-api-tools/api-tools. The following steps describe, how to define an endpoint in your module.config.php file of your application.

Routing

The routing is like normal routing. You did that already with your implementation. Just write a router configuration in your application config config/module.config.php.

return [
    ...
    'router' => [
        'routes' => [
            'application.rest.user' => [
                'type': \Laminas\Router\Http\Segment::class,
                'options' => [
                    'route' => '/user[/:user_id]',
                    'defaults' => [
                        'controller' => 'Application\\V1\\Rest\\User\\Controller',
                    ],
                ],
            ],
        ],
    ],
    ...
];

Of course, you could try to make the route more dynamic so that you don’t have to make a separate router entry for each resource. In this example, however, we will keep it very simple and assume that there is a route entry for each existing resource.

Versioning

API Tools ship a version control with laminas-api-tools/api-tools-versioning. For how to configure the access to a specific version of your API resources just have a look at the versioning documentation. For our purpose we just use the uri key for routes, that can have a [/v:version] in it 's route.

...
'api-tools-versioning' => [
    'uri' => [
        'application.rest.user',
    ],
],
...

Rest Configuration

Since laminas-api-tools/api-tools installed the dependency module laminas-api-tools/api-tools-rest we can easily define our rest service. Just add the api-tools-rest key to your config/module.config.php of your application.

...
'Application\\V1\\Rest\\User\\Controller' => [
    'listener' => \Application\V1\Rest\User\UserResource::class,
    'route_name' => 'application.rest.user',
    'route_identifier_name' => 'user_id',
    'collection_name' => 'user',
    'entity_http_methods' => [
        0 => 'GET',
        1 => 'PATCH',
        2 => 'PUT',
        3 => 'DELETE',
    ],
    'collection_http_methods' => [
        0 => 'GET',
        1 => 'POST',
    ],
    'collection_query_whitelist' => [],
    'page_size' => 25,
    'page_size_param' => null,
    'entity_class' => \Application\V1\Rest\User\UserEntity::class,
    'collection_class' => \Application\V1\Rest\User\UserCollection::class,
    'service_name' => 'User',
],
...

These settings define some major points for your rest service. Under the listener key you define the resource class. With every call of the defined route route_name this class will be dispatched to handle all entity or collection requests. As you can see you don 't need a controller class for your REST service. All will be handled in the class we mentioned under the listener key. For a full description of all available user config keys have a look at the github repository.

Content negotiation

Attention! At this point, we’ll get into the topic of your interface’s security a bit. Here we define which controller should respond with which type and what media types are accepted. Like all other configuration just add this to your application config/module.config.php file.

...
'api-tools-content-negotiation' => [
    'controllers' => [
        'Application\\V1\\Rest\\User\\Controller' => 'Json',
    ],
    'accept_whitelist' => [
        'Application\\V1\\Rest\\User\\Controller' => [
            'application/vnd.my-api-name.v1+json',
            'application/json',
            'application/*+json',
        ],
    ],
    'content_type_whitelist' => [
        'Application\\V1\\Rest\\User\\Controller' => [
            'application/vnd.my-api-name.v1+json',
            'application/json',
        ],
    ],
],
...

From my point of view, the settings are enormously important in terms of security. Hereby you ensure which Accept Header information and which Content Type is accepted by your API. Everything that does not match these settings will be answered with a 406 Not Accetbale or a 415 Unsupported Media Type. For a complete list of content negotiation settings have a look at the respective github repository.

Content Validation

In this example we define a user service. This service can deal with POST and PUT/PATCH requests for creating or updating data sets. The good old principle “never trust user data” forces us to validate the received data as strictly as possible. For this we use the input filters and validators from the Laminas framework. You might know that from laminas/laminas-form. It 's quite identical when dealing with API tools. As always add the following code to your applications config/modules.config.php.

...
'api-tools-content-validation' => [
    'Application\\V1\\Rest\\User\\Controller' => [
        'input_filter' => 'Application\\V1\\Rest\\\User\\Validator',
    ],
],
'input_filter_specs' => [
    'Application\\V1\\Rest\\\User\\Validator' => [
        [
            'name' => 'id',
            'required' => false,
            'filters' => [ ... ],
            'validators' => [ ... ],
        ],
    ],
],
...

This is the simplest form of adding content validation. You can define a single input filter spec for every http method allowed for your service. For a deeper understanding have a look at the configuration specs in the github respository.

Authorization

The laminas-api-tools/api-tools module also installed the laminas-api-tools/api-tools-mvc-auth module. This module provides services, event listener and configuration for handling authentication and authorization. There are different ways of authentication: Bais authentication, Digest authentication and OAuth2 authentication. There is no simple way of just logging in to an existing user account. Common APIs do not work that way. I personally prefer the OAuth2 way. It 's secury and widely known.

Authentication can be pretty complex. All you have to know is well written down in the github repository documentation.

Conclusion

If you want an easy to setup API based on Laminas API Tools I strongly recommend using the laminas-api-tools/api-tools-skeleton repository. It 's easy to use and well documented. Creating an API isn 't just about setting up a restful controller and trying to respond with the right http status codes. As you can see above, there 's a lot more to recognize.

Considering the development time you need to programmatically recreate all of this, it clearly makes more sense from my point of view to simply use the ready-made UI, through which you can set up REST and RPC services in a relatively short time. There are good tutorials on how to implement authentication and the associated authorization. At the risk of repeating myself, just use the admin UI and read the tutorials. It’s not going to get any easier.