Custom endpoints and API access

Most data fetching should happen in custom streams but there might be some cases where you still want to define a custom endpoint, especially when change operations must be executed on third party services. An example could be registering a user on a third party newsletter service.

Registering a custom controller

Our backend stack is a default Symfony stack so the common mechanisms apply, while some configuration files can be found in different places. Let's take a look at how to create a new route following the example of registering a user with a third party newsletter service.

Create a route

Each controller action is identified by a route. The configuration file <customer>_<project>/config/customer_routing.yml can be used to reference routing configurations in your bundle. If you have a bundle like a MyDecoratorsBundle the following lines should be added to the configuration file (if there isn't a bundle yet, you can create one using bin/console frontastic:create:bundle MyDecorators, for example):

my_decorators: # this must be unique
    resource: "@AcmeMyDecoratorsBundle/Resources/config/routing.xml"
    prefix: /api/newsletter

The prefix will be applied to all URLs you specify inside the references routing.xml file. An "empty" prefix like / would also work. You'll also need to add a route to the routing.xml inside the bundle. This will be located in a place like <customer>_<project>/src/php/MyDecoratorsBundle/Resources/config/routing.xml and with a single route that could look like:

<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="Acme.MyDecorators.Newletter.register" path="/register" methods="POST">
        <default key="_controller">Acme\MyDecoratorsBundle\Controller\NewsletterController::registerAction</default>
    </route>
</routes>

Together with the prefix, this creates the URL /api/newsletter/register and all POST requests to this URL will be passed on to the controller we configured here and we still have to write. The actual URL is nothing you have to remember because the routing IDs will also be available in the frontend to ensure consistent URLs which are only defined in a single place. So the key to remember is Acme.MyDecorators.Newletter.register in this case. If you want to learn more about routing in Symfony go to their documentation on this topic.

You can debug the routes by running the following command in your Docker container on your Virtual Machine:

<customer>_<project>/ $ bin/console debug:router
[...]
Acme.MyDecorators.Newletter.register    POST    ANY    ANY    /api/newsletter/register
[...]

If the route shows up in this list, you should be good to go.

Write the controller

Now, let's write the controller so we get some action behind the specified URL. We need to create the controller class with the name specified in the routing configuration Acme\\MyDecoratorsBundle\\Controller\\NewsletterController, which usually would go somewhere like <customer>_<project>/src/php/MyDecoratorsBundle/Controller/NewsletterController.php and look like:

<?php

namespace Acme\MyDecoratorsBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

use Frontastic\Catwalk\ApiCoreBundle\Domain\Context;

class NewsletterController extends Controller
{
    public function registerAction(Context $context, Request $request): JsonResponse
    {
        // @TODO: Actually trigger newsletter registration with data from the $request

        return new JsonResponse(['ok' => true]);
    }
}

More on Symfony controllers can again be found in their documentation. We convert random return values (like objects, arrays, or scalar values) into a JsonResponse automatically if the request headers include Accept: application/json or the request is an XHR request. But it might be easier to debug to explicitly always return a JsonResponse.

The PHP code to write here is up to you. While we strongly suggest to define and use a custom service for the actual task (registering a user in a third party newsletter service in our example) you could also do this right here in the Controller.

Use the new endpoint

As mentioned new endpoints can be used by their route ID which is exposed to the frontend framework. How you want to integrate this exactly into your React components in the frontend is up to you, you could use a hook or any other method. The main point is using our api wrapper, which knows these route IDs:

import React, { Component } from 'react'

import app from '@frontastic/catwalk/src/js/app/app'

class MyComponent extends Component {
    registerUser = (name, email) => {
        app.getApi().request(
            'POST', // HTTP method
            'Acme.MyDecorators.Newletter.register', // Route ID
            {}, // Query parameters
            { name, email }, // HTTP body (will be JSON encoded)
            (responseJson) => { // Success callback with decoded JSON
                // @TODO: Do something (like setting component state)
            },
            (error) => { // Error callback with decoded JSON
                // @TODO: Do something (like setting component state)
            }
        )
    }

    render () {
        return <div>Render something...</div>
    }
}

export default MyComponent

This is just one possible example of using the api inside a React component. The app.getApi() function returns an instance of the API which is aware of all Symfony routes. This means that the request method can be called with the following parameters:

  • HTTP method, usually something like: GET, POST
  • Symfony route ID, as configured in the routing.xml
  • Optional hash map with query and route parameters (often called GET parameters)
  • Optional body content which will automatically be JSON encoded
  • Success callback which will receive the decoded response from the server
  • Error callback which will receive the decoded (if possible) response from the server

API access

As mentioned before most API access or fetch operations should happen in custom streams but there might be some cases where you want to implement write operations or asynchronous load operations in custom controllers. The most common write operations are covered by the Frontastic loaders.

Optionally, you can directly access these APIs in your PHP code so that you can use our APIs to implement additional actions. You can also retrieve the internal client, for example, the commercetools client, to perform authorized HTTP calls against commercetools which aren't covered by our APIs. You can access an API from a controller, like:

<?php

namespace Acme\MyDecoratorsBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

class SomeController extends Controller
{
    public function someAction(Request $request): JsonResponse
    {
        $productApi = $this->get('frontastic.catwalk.product_api');

        return new JsonResponse([
            'products' => $productApi->query(/* &hellip; */),
        ]);
    }
}

The APIs which are available this way are:

  • frontastic.catwalk.account_api
  • frontastic.catwalk.cart_api
  • frontastic.catwalk.content_api
  • frontastic.catwalk.product_api
  • frontastic.catwalk.wishlist_api

These could be a dependency of one of your services configured in the dependency injection container. To get access to the authorized client which is used for a certain API, they have the method getDangerousInnerClient()use this method with care as we don't guarantee any backwards compatibility for the return value of this method and especially not for the client itself.


‹ Back to article list

Next article ›

Customize product URLs

Still need help? Contact us Contact us