Let's Connect

A whiteboard sketch comparing two API architectures, representing the REST versus GraphQL decision for a Laravel backend

The rest vs graphql laravel decision usually starts from a wrong premise: that GraphQL is the newer, better REST. It is not an upgrade. It is a different set of tradeoffs, and for most Laravel APIs REST is still the correct default. GraphQL earns its complexity in exactly one situation: when client query flexibility is your actual problem, not a hypothetical one. If your clients are happy hitting predictable endpoints and you can describe their needs in a handful of resources, reach for API Resources and move on. If you have a mobile app, a web SPA, and a partner integration all hammering the same data graph and each wanting different field shapes, GraphQL starts to pay for itself. I have shipped both, and I have also ripped out a GraphQL layer that was solving a problem nobody had. Here is how to decide before you write the schema.

Why is REST the right default for a Laravel API?

REST is boring in the way good infrastructure is boring. Every proxy, CDN, browser, and HTTP client on the planet already understands it. A GET request with the right cache headers is cacheable at the edge for free; you do not write a line of code for Cloudflare or Varnish to serve it. Versioning is a route prefix. And Laravel's API Resources give you a clean transformation layer between your Eloquent models and the JSON you actually want to expose, so you are never accidentally leaking a column.

The shape of a REST response is decided on the server, which is the thing people frame as a weakness. In practice it is a feature: the contract is predictable, you can document it once, and your error handling lives in one place. If you are building a decoupled frontend against this, the patterns I use are in the Next.js and Laravel decoupled architecture guide.

app/Http/Resources/PostResource.php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'        => $this->id,
            'title'     => $this->title,
            'excerpt'   => $this->excerpt,
            // whenLoaded prevents an N+1: author is only serialized
            // if the controller eager-loaded it.
            'author'    => UserResource::make($this->whenLoaded('author')),
            'published' => $this->published_at?->toIso8601String(),
        ];
    }
}

Note the whenLoaded() call. That single helper is how REST sidesteps the over-fetching argument: the controller decides what to eager-load, and the resource only serializes relations that are actually present. No relation loaded means no extra query and no leaked data. The client gets a stable, documented payload every time.

What does GraphQL actually solve?

GraphQL fixes one specific pain: clients fetching exactly the fields they need from one endpoint against a typed schema. No over-fetching a fat object when the list view only needs a title. No under-fetching that forces three round trips to assemble a screen. When you have genuinely varied clients, a mobile app that wants three fields and a web dashboard that wants thirty, both reading the same graph, you stop building one bespoke endpoint per screen. The client composes the query; the server exposes the graph.

In Laravel the mature path is Lighthouse, which is schema-first: you define types and fields in an SDL file and annotate them with directives that wire up to Eloquent. A field-for-field equivalent of the resource above is compact.

graphql/schema.graphql
type Post {
  id: ID!
  title: String!
  excerpt: String
  publishedAt: DateTime @rename(attribute: "published_at")
  # @belongsTo batches the author lookup so 50 posts do
  # not become 50 separate author queries (the N+1 trap).
  author: User @belongsTo
}

type Query {
  # Built-in pagination + filtering, no resolver code.
  posts(title: String @where(operator: "like")): [Post!]! @paginate
  post(id: ID! @eq): Post @find
}

That @belongsTo directive is doing real work. Without batching, a query for 50 posts each asking for their author fires 50 follow-up queries. Lighthouse batches relation fields together, collecting the IDs and resolving them in a single query (you can disable this with the batchload_relations config flag, but you almost never should). This is the central operational gotcha of GraphQL, so it deserves its own section.

A developer at a desk reviewing API query structures on a monitor, weighing field-level data fetching against endpoint design
Choosing the API style is a data-access decision first and a syntax decision second. The query shape your clients need should drive it.

What does GraphQL cost you?

Every benefit above has a bill attached, and you pay it in operational complexity that REST never charges you for. None of these are dealbreakers, but you have to budget for all of them on day one, not discover them in production.

  • The N+1 problem. A flexible nested query can fan out into hundreds of database hits unless every relation goes through a batched loader. Lighthouse's relation directives handle the common cases, but any custom resolver you write can quietly reintroduce it.
  • HTTP caching gets harder. Everything is a POST to one endpoint, so you lose free edge and browser caching by URL. You move caching into the application layer instead — see my notes on Redis caching strategies.
  • Authorization moves to the field level. With one endpoint you cannot gate access per route. Lighthouse gives you the @can directive and policy hooks, but you have to reason about authorization on individual fields, which is more surface area to get right.
  • Query abuse is a real attack vector. A client can request deeply nested, cyclic, or enormous queries to exhaust your server. You must cap query depth and complexity, and that throttling concern is related to how you would otherwise design API rate limits per user.

The query-complexity point is the one teams forget. With REST your endpoints define the maximum cost of any request. With GraphQL the client defines the cost, so you have to impose limits server-side or hand every user a denial-of-service primitive. Lighthouse ships with depth and complexity validation that is disabled by default; you set real limits in config/lighthouse.php and turn it on before you expose the schema.

config/lighthouse.php
<?php

use GraphQL\Validator\Rules\DisableIntrospection;

return [
    // ... other Lighthouse config ...

    'security' => [
        // Reject queries that nest deeper than this. 0 (the default)
        // disables the check, so set a concrete cap.
        'max_query_depth' => 10,

        // Reject queries whose computed cost exceeds this. 0 disables it.
        'max_query_complexity' => 200,

        // Disable introspection on public-facing schemas so attackers
        // cannot trivially map your entire graph.
        'disable_introspection' => DisableIntrospection::ENABLED,
    ],
];
GraphQL does not remove complexity from your API. It moves it from the URL to the schema and hands part of it to the client. If you are not ready to own query-cost limits and field-level authorization, you are not ready for GraphQL.Md Raihan Hasan

So which one should I choose?

Decide on the data-access pattern, not on which technology sounds more modern. The honest version of this is short.

Choose REST if

  • Your clients are happy with predictable, documented endpoints and you can model their needs as a manageable set of resources.
  • HTTP-level caching matters — public read-heavy endpoints behind a CDN benefit enormously from cacheable GETs.
  • You want the broadest possible tooling and the lowest onboarding cost for new developers and integrators.
  • Your team is small and you would rather spend complexity budget on the product than on a query layer.

Choose GraphQL if

  • You have multiple distinct clients (mobile, web, partners) reading the same graph with genuinely different field needs.
  • Over-fetching and under-fetching are measured, recurring problems — not a hypothetical you are pre-optimizing for.
  • A strongly typed, self-documenting schema and rapid client-driven iteration are worth real money to your product.
  • You have the appetite to own batched loaders, per-field authorization, and query-complexity limits from day one.

There is also a legitimate middle path I have shipped more than once: a REST API as the default, with a single GraphQL endpoint added only for the one client whose query needs are genuinely variable. You do not have to pick a religion. Start with REST API Resources because they are cheap, cacheable, and predictable. Reach for Lighthouse the day you can point at a real, measured over-fetching problem across multiple clients — and when you do, set depth and complexity limits in the same commit that exposes the schema. The right answer is whichever one matches how your clients actually read your data, and for most Laravel APIs that is still REST.