DEV Community

Carlos Talavera
Carlos Talavera

Posted on

Transforming API requests and responses in Laravel 11 - The easy way

The problem

I've been working on an application using Next.js on the front-end and Laravel on the back-end as a traditional REST API. As you may know, snake_case is the naming convention for variable and function names in PHP, while camelCase is the naming convention in JavaScript. My database tables and columns use snake_case as well, so I stuck to that design.

A first approach: Laravel resources

I was already using Laravel resources to return clean API responses. For each API response we can specify each key using camelCase. Something like:

class ProductResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'price' => $this->price,
            'stock' => $this->stock,
            'category' => $this->category,
            'imageUrl' => $this->image_url,
            'salesCount' => $this->sales_count,
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

While this is valid, it could be highly demanding for us to format every API response to camelCase, and even more when we have Eloquent relationships that we have to format too. We could also say "Well, let's create a BaseResource that extends the JsonResouce class, which formats everything to camelCase, and then have every resource class extend from there". It would be something like this:

class BaseResource extends JsonResource
{
    /**
     * Convert array keys from snake_case to camelCase.
     *
     * @param array $array
     * @return array
     */
    protected function toCamelCase($array)
    {
        $camelCaseArray = [];
        foreach ($array as $key => $value) {
            // Here we use the Str::camel() method from Laravel
            $camelKey = Str::camel($value);
            // Recursively convert nested arrays
            $camelCaseArray[$camelKey] = is_array($value) ? $this->toCamelCase($value) : $value;
        }
        return $camelCaseArray;
    }

    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return $this->toCamelCase(parent::toArray($request));
    }
}
Enter fullscreen mode Exit fullscreen mode

(Note the need for a recursive approach to deal with nested arrays).

That way, we have two scenarios: one where we simply extend from that class to display the visible fields from a table as they are, i. e., without any formatting for relationships or hidden fields excluded from the API response; and the second scenario where we do want to format our API response. The first case would look something like this:

class UserResource extends BaseResource
{
    // No need for anything in here
}
Enter fullscreen mode Exit fullscreen mode

The second case would be the ProductResource class already showed, but with key differences: it preserves snake_case, extends the BaseResource class and implements the toCamelCase() method:

class ProductResource extends BaseResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return $this->toCamelCase([
            'id' => $this->id,
            'name' => $this->name,
            'price' => $this->price,
            'stock' => $this->stock,
            'category' => $this->category,
            'image_url' => $this->image_url,
            'sales_count' => $this->sales_count,
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

That seems nice too. So, what's the catch? To me, there are two inconveniences: on one side, if we already have several resources we would have to change them all to extend BaseResource and use toCamelCase() where it applies; and on the other side, we should be really careful all the time while creating resources so they extend BaseResource and not JsonResource, and apply the conversion method if needed. We could modify the Artisan command for creating resources so they extend BaseResource instead of JsonResource. But all of that seems like too much work to me.

There's also one more thing, what if our API expects requests using snake_case? We could format API responses to camelCase, but we should format them back again to snake_case from our Next application. While Next supports server-side rendering, it doesn't sound like a good option to leave all that work to what is supposed to be the front-end. So, what can we do?

The solution: Laravel middlewares

To me, a middleware is basically something that intercepts traffic on a higher level and does stuff with it (I know it's vague, but why complicate things?). With higher level I mean that, unlike a proxy, a middleware does not work with a raw HTTP protocol, but does something with HTTP information already parsed.

What we want to achieve is to use an input middleware and an output middleware. One for "what we are receiving" and one for "what we are replying". We'll call the input one TransformApiRequest, so it handles the camelCase to snake_case conversion. The output one will be called TransformApiResponse, so it handles the snake_case to camelCase conversion. This way we keep our naming conventions within our Laravel application and just transform the information to the way we need it to be. Besides, all that transformation logic lives in the same place, making it easier to maintain.

Using Laravel 11, we create these middlewares as follows:

php artisan make:middleware TransformApiRequest
php artisan make:middleware TransformApiResponse

I'm not using Docker, so I'm not utilizing Laravel Sail. However, if you're using it, simply replace php with sail.

The TransformApiRequest middleware would look something like this:

class TransformApiRequest
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next)
    {
        // Transform keys of requests that are not GET to snake_case
        if ($request->method() !== 'GET') {
            $input = $request->all();
            $transformedInput = $this->transformKeysToSnakeCase($input);
            $request->replace($transformedInput);
        }

        return $next($request);
    }

    /**
     * Transform keys of an array to snake_case.
     *
     * @param  array  $input
     * @return array
     */
    private function transformKeysToSnakeCase($input)
    {
        $result = [];
        foreach ($input as $key => $value) {
            // Here we use the Str::snake() method from Laravel
            $snakeKey = Str::snake($key);
            $result[$snakeKey] = is_array($value) ? $this->transformKeysToSnakeCase($value) : $value;
        }
        return $result;
    }
}
Enter fullscreen mode Exit fullscreen mode

In my case, I didn't want to convert GET requests with query parameters to snake_case because some of the parameters needed to stay in camelCase since I use (with no promotion intended) Laravel Purity for filtering. So, when it comes to Eloquent relationships, camelCase is the way to go. The other logic is self-explanatory.

As for the TransformApiResponse middleware, it's just a little bit more complex because we're not dealing with incoming requests, but with responses, so we need a way to identify when we need to format the response to camelCase. A clear condition is that the Content-Type header of our response must be application/json. But maybe we only want to transform successful responses, which makes the most sense to me, because, why would we have a complex error JSON response with keys long enough to be different in snake_case and camelCase? So let's consider that condition too. This middleware would look something like this:

class TransformApiResponse
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);

        // Transform keys of successful JSON responses to camelCase
        if ($response->isSuccessful() && $response->headers->get('Content-Type') === 'application/json') {
            $data = json_decode($response->getContent(), true);
            if ($data) {
                $transformedData = $this->transformKeysToCamelCase($data);
                $response->setContent(json_encode($transformedData));
            }
        }

        return $response;
    }

    /**
     * Transform keys of an array to camelCase.
     *
     * @param  array  $data
     * @return array
     */
    private function transformKeysToCamelCase($data)
    {
        $result = [];
        foreach ($data as $key => $value) {
            // Here we use the Str::camel() method from Laravel
            $camelKey = Str::camel($key);
            $result[$camelKey] = is_array($value) ? $this->transformKeysToCamelCase($value) : $value;
        }
        return $result;
    }
}
Enter fullscreen mode Exit fullscreen mode

$response->isSuccessful() verifies that the response has a status code between 200 and 299. The other logic is self-explanatory.

Now we need to add these middlewares within our application. In Laravel 11, we go to bootstrap/app.php and locate the withMiddleware() method. As we're working on an API, we need to append these new middlewares to the api middleware group as follows:

->withMiddleware(function (Middleware $middleware) {
        // Transform keys of requests that are not GET to snake_case
        // and keys of successful JSON responses to camelCase
        $middleware->appendToGroup('api', [
            TransformApiRequest::class,
            TransformApiResponse::class,
        ]);
    })
Enter fullscreen mode Exit fullscreen mode

That's it. With this approach, now we can keep developing using snake_case in Laravel and camelCase in Next.js seamlessly :)

"Drawbacks"

While it is true that the use of middlewares has an impact on performance, it is also true that we should keep API responses and requests as clean and short as possible. Thus, transforming information from snake_case to camelCase and vice versa should not be the culprit of a slow application.

Conclusions

Laravel is a great framework with a wide set of built-in features that make our life easier, but sometimes we have to decide which is the best way to go to reduce complexity and achieve our goals. In this case, it seemed that using Laravel resources would be a good option at first, but then, using custom middlewares turned out to be more sustainable and easier to implement.

This is my first article, so any feedback is well appreciated. I hope you liked it and found it useful :D

Top comments (2)

Collapse
 
dharma_farina_084f309e8f2 profile image
Dharma Farina

I found interesting the way of approaching the problem, in a way now it is clearer to me because I think that many times we have forgotten to focus on details like this and considering them helps a lot to improve the value of our code. :)

Collapse
 
charliet1802 profile image
Carlos Talavera

Thank you!