API Resources & Building a REST API

A huge number of Laravel applications aren't just serving HTML pages — they're also (or entirely) serving JSON to a mobile app, a JavaScript frontend like Vue or React, or a third-party integration. Laravel makes building a JSON API pleasant, but the naive approach of just returning an Eloquent model directly has real problems once your app is more than a toy. API Resources are Laravel's answer: a clean, explicit transformation layer between your database columns and the JSON your API actually promises to clients. This page builds a small but complete Articles API from scratch, and covers the HTTP status codes and authentication concepts that go with it.

routes/api.php vs. routes/web.php

Every Laravel project ships with two separate route files, and understanding why matters. You already met routes/web.php in Routing Basics — those routes go through the web middleware group, which includes session handling, CSRF protection, and cookie encryption, because they're built to serve a browser rendering HTML. routes/api.php is different on purpose: it's stateless (no sessions, no CSRF token requirement) and every route defined there is automatically prefixed with /api. That's exactly what you want for routes meant to be called by code — a mobile app, a single-page app's fetch() calls, or another server — rather than by a browser following links and submitting forms.

<?php
// routes/api.php
use App\Http\Controllers\ArticleController;

Route::get('/articles', [ArticleController::class, 'index']);
Route::get('/articles/{article}', [ArticleController::class, 'show']);

// Both of the routes above are actually reachable at:
//   GET /api/articles
//   GET /api/articles/5

The Problem With Returning a Model Directly

Laravel is convenient enough that this actually "works" the very first time you try it:

<?php
public function show(Article $article)
{
    return $article;
}

When a controller method returns an Eloquent model (or a collection of models), Laravel automatically converts it to JSON using the model's toArray() method. That's genuinely useful for a quick prototype, but it has a real downside in anything beyond one: it exposes every single column on the table, exactly as stored. If your articles table has an internal editor_notes column, a views_count field you don't want to commit to as a public API contract, or timestamp columns you'd rather rename in the output, none of that is under your control. Worse, if a related model like the article's author is loaded, its own hidden or sensitive columns (like a hashed password) can leak into the response unless you remembered to mark them $hidden on that model too. Relying on "whatever the table happens to contain" as your public API shape is fragile — a database migration that adds a column can silently change your API's response for every client.

Generating and Writing a Resource

An API Resource is a plain PHP class whose only job is to describe how one model (or a collection of them) should be turned into an array before it becomes JSON. Generate one with Artisan:

php artisan make:resource ArticleResource

This creates app/Http/Resources/ArticleResource.php with a single method to fill in, toArray($request), which returns the exact array you want serialized to JSON:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class ArticleResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'excerpt' => str($this->body)->limit(120),
            'author' => $this->author->name,
            'published_at' => $this->created_at->toDateString(),
        ];
    }
}

Notice exactly what's happening here: the resource picks id and title straight from the model, reshapes body into a short excerpt, reaches through the author relationship to pull out just the author's name (not the whole related user record), and formats the timestamp. Every column not explicitly listed — including anything sensitive or internal — simply never appears in the response. The resource is the single source of truth for what your API's JSON contract looks like, completely decoupled from what your database table happens to contain.

Using a Resource in a Controller

Wrap a single model in the resource class to return one item, or call the static ::collection() helper to return many — it handles wrapping every item in the collection through the same resource automatically:

<?php

namespace App\Http\Controllers;

use App\Http\Resources\ArticleResource;
use App\Models\Article;

class ArticleController extends Controller
{
    public function index()
    {
        $articles = Article::with('author')->latest()->paginate(15);

        return ArticleResource::collection($articles);
    }

    public function show(Article $article)
    {
        return new ArticleResource($article);
    }
}
Note: Eager-loading author with with('author') before the resource ever runs matters — the resource's toArray() above reaches into $this->author->name, and without eager loading that would fire one extra query per article (the classic N+1 problem).

HTTP Status Codes

Returning a resource directly from a controller defaults to a 200 OK status, which is correct for reading data — but a real API needs to communicate more than just "success" or "here's some JSON." Use response()->json() when you need to set the status explicitly, most commonly after creating a resource or when something couldn't be found:

<?php
public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'body' => 'required|string',
    ]);

    $article = Article::create($validated);

    return response()->json(new ArticleResource($article), 201);
}

public function show($id)
{
    $article = Article::find($id);

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

    return new ArticleResource($article);
}
StatusMeaningTypical use
200 OKSuccessA normal GET request that found data
201 CreatedResource createdA successful POST that made a new record
204 No ContentSuccess, nothing to returnA successful DELETE
401 UnauthorizedNot authenticatedMissing or invalid API token
403 ForbiddenAuthenticated, but not allowedA Policy denies the action
404 Not FoundNo such resourceRequested ID doesn't exist
422 Unprocessable EntityValidation failedLaravel's default for failed validate() calls on an API request
500 Internal Server ErrorUnhandled server errorAn uncaught exception

A Complete Worked Example

Putting the route, controller, and resource together gives you a small, real JSON API for reading articles:

<?php
// routes/api.php
Route::get('/articles', [ArticleController::class, 'index']);
Route::get('/articles/{article}', [ArticleController::class, 'show']);

// A client calling GET /api/articles gets back:
// {
//   "data": [
//     { "id": 1, "title": "Hello Laravel", "excerpt": "...", "author": "Sonu", "published_at": "2026-08-01" }
//   ]
// }

Note the automatic "data" wrapper key around the collection — JsonResource adds that by default, which is a widely used convention that leaves room to add sibling keys later (like pagination metadata) without breaking clients already reading data.

Authenticating an API: a Word on Sanctum

Everything above assumes anyone can call the endpoint, which is fine for public read-only data but not for anything that mutates state or is user-specific. Laravel Sanctum is the framework's lightweight, official solution for simple token-based API authentication — a mobile app or SPA logs in once, receives a personal access token, and then sends it on every subsequent request in an Authorization: Bearer <token> header. Laravel checks that token against the personal_access_tokens table and, if it's valid, authenticates the request as that user — no cookies, no sessions. Protecting a route then becomes as simple as attaching the auth:sanctum middleware, conceptually similar to the middleware you saw in Middleware. Sanctum's full setup (installing it, issuing tokens, configuring SPA authentication) is its own topic — this page is only meant to make sure the concept and the vocabulary are familiar when you go looking for it.

Up next: With a working API in place, it's time to make sure it keeps working — head to Testing to learn how Laravel lets you write automated tests for routes just like these.