Transforming API requests and responses in Laravel 11 - The easy way
Tags: laravel, php, javascript, webdev
Published: 2024-05-14 17:26:49

## The problem

I've been working on an application using [Next.js](https://nextjs.org) on the front-end and [Laravel](https://laravel.com) 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](https://laravel.com/docs/11.x/eloquent-resources) to return clean API responses. For each API response we can specify each key using camelCase. Something like:

```php
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,
        ];
    }
}
```
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 ](https://laravel.com/docs/11.x/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:

```php
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));
    }
}
```
(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:

```php
class UserResource extends BaseResource
{
    // No need for anything in here
}
```
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:

```php
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,
        ]);
    }
}
```
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](https://laravel.com/docs/11.x/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](https://nextjs.org/docs/pages/building-your-application/rendering/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](https://www.docker.com), so I'm not utilizing [Laravel Sail](https://laravel.com/docs/11.x/sail). However, if you're using it, simply replace `php` with `sail`.

The `TransformApiRequest` middleware would look something like this:

```php
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;
    }
}
```

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](https://github.com/abbasudo/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:

```php
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;
    }
}
```

`$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:

```php
->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,
        ]);
    })
```

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