# Writing JSON API Endpoints with Laravel

> A practical walkthrough of building interface-agnostic JSON API endpoints in Laravel.

- Published: 2015-05-03
- Category: Web Development
- Tags: Laravel, API
- Reading time: 4 min read
- Source: https://www.muhammetsafak.com.tr/en/blog/writing-json-api-endpoints-with-laravel/
- Language: en-US
- Author: Muhammet Şafak

---
When I switched from generating [Blade templates](/en/blog/blade-template-engine-eliminating-repetitive-html) to returning JSON in Laravel, the first challenge was: "how do I structure this?" It requires a different way of thinking compared to a classic web app where forms POST to actions and responses are redirects. The client is no longer a browser — it might be a mobile app, another service, or JavaScript code firing AJAX requests.

In this post I'll walk through how I write JSON API endpoints with [Laravel 5](/en/blog/migrating-to-laravel-5-directory-structure-and-env), how I organize the response structure, and how I handle the situations I run into most often.

## Defining Routes

In Laravel 5, a good habit is to put API routes in their own group inside `app/Http/routes.php`. It makes it easy to add a URL prefix (`/api/v1/...`) and to apply middleware cleanly.

```php
Route::group(['prefix' => 'api/v1'], function () {
    Route::get('orders',         'Api\OrderController@index');
    Route::post('orders',        'Api\OrderController@store');
    Route::get('orders/{id}',    'Api\OrderController@show');
    Route::put('orders/{id}',    'Api\OrderController@update');
    Route::delete('orders/{id}', 'Api\OrderController@destroy');
});
```

I use the `Api\` namespace to keep API controllers separate from regular web controllers. Files under `app/Http/Controllers/Api/` pick up this prefix.

## Returning JSON Responses

You return a JSON response in Laravel with `response()->json()`. The second parameter sets the status code.

```php
class OrderController extends Controller
{
    public function index()
    {
        $orders = Order::all();

        return response()->json([
            'data' => $orders,
        ], 200);
    }

    public function store(Request $request)
    {
        $order = Order::create($request->only('product_id', 'quantity'));

        return response()->json([
            'data' => $order,
        ], 201);
    }

    public function show($id)
    {
        $order = Order::find($id);

        if (!$order) {
            return response()->json([
                'message' => 'Order not found.',
            ], 404);
        }

        return response()->json([
            'data' => $order,
        ], 200);
    }
}
```

One thing I'm deliberate about: always using a `data` key in every response. At first I thought "why add this extra layer?" but once the client side has a consistent response structure, a clear contract emerges — "if `data` is my payload then errors live under a separate key." It makes consumption predictable.

## Standardizing Error Responses

Returning error responses in a single, consistent format across the whole application makes life easier for whoever is writing the client. A single helper method does the job:

```php
protected function errorResponse($message, $code = 400): \Illuminate\Http\JsonResponse
{
    return response()->json([
        'error'   => true,
        'message' => $message,
    ], $code);
}
```

Then inside any controller: `return $this->errorResponse('Invalid data.', 422);`. No inconsistent formats scattered across controllers.

Moving this into a dedicated trait rather than a base controller can be cleaner. That way any controller that needs the behavior can pull it in without being forced into a particular inheritance hierarchy.

## Validation and APIs

[Laravel's validation system](/en/blog/laravel-form-validation-in-practice) normally redirects on failure. In an API we want a JSON error response, not a redirect.

The `FormRequest` class handles this. Create a `StoreOrderRequest` and drop the `wantsJson()` check — Laravel 5 automatically returns a 422 JSON error response for JSON requests.

```php
class StoreOrderRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'product_id' => 'required|integer|exists:products,id',
            'quantity'   => 'required|integer|min:1',
        ];
    }
}
```

Type-hinting it in the controller is all you need:

```php
public function store(StoreOrderRequest $request)
{
    // If we reach this point, validation has passed
    $order = Order::create($request->only('product_id', 'quantity'));

    return response()->json(['data' => $order], 201);
}
```

When validation fails, Laravel automatically produces a response like this:

```json
{
  "product_id": ["The product field is required."],
  "quantity": ["The quantity field is required."]
}
```

with a 422 status code.

## Setting the Content-Type Header Correctly

On the client side, don't forget to include `Content-Type: application/json` when sending JSON. Laravel looks at this header to parse the request body as JSON. Without it, `$request->input()` won't reach your values.

If the header is missing, Laravel treats the request as form data. You send a JSON body, but `$request->input('product_id')` returns null — and it takes a while to figure out why. When testing with Postman, always set both `Content-Type: application/json` and `Accept: application/json`. The second one tells Laravel to return validation errors as JSON; leave it out and you might get an HTML error page instead.

## My First Real API Experience

When I built a small order API this way and connected it to a simple iOS app, it felt remarkably clean. The server side just returns data — it doesn't need to know how it will be displayed. The client side just receives and renders data — it doesn't need to know how it was produced.

That separation pays off as the application grows. A web interface, a mobile app, and another service can all consume the same API. You write the logic once.
