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,
];
}
}
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));
}
}
(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
}
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,
]);
}
}
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;
}
}
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;
}
}
$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,
]);
})
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 (6)
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. :)
Thank you!
Very helpful, Thank you for posting
Thank you for reading it!
Good article, thanks!
I'm glad you liked it!