Product Decisions This Supports
API architecture: BFF / public API surface. This package lets us ship a GraphQL layer entirely inside the Laravel application — no separate Node/Apollo service, no second deploy target, no second on-call rotation. That makes "GraphQL as our API contract" a single-team decision instead of a platform-org decision. Concretely it supports:
- Frontend-driven delivery. Client teams request exactly the fields they need (mobile payload trimming, web dashboards, partner integrations) without us versioning REST endpoints per consumer. This is the classic "we have 4 clients and 4 diverging REST contracts" problem, and it's the strongest reason to adopt.
- Build vs. buy: buy the plumbing, own the schema. The package is an MIT wrapper over
webonyx/graphql-php, so we buy routing, validation integration, authorization hooks, error formatting, security controls, and observability — while the schema (our actual product contract) stays as plain PHP classes in our repo. That's the right split: commodity code off the shelf, differentiating model on our side.
- Roadmap sequencing. Because schema definitions live in PHP classes with Artisan generators (
make:graphql:type|query|mutation|input|enum|interface|union|scalar|field), incremental domain-by-domain rollout is cheap. We can expose Books in Q1, Orders in Q2, and never do a big-bang migration. Good fit for a phased roadmap with per-slice acceptance.
- Multi-surface products. Multiple schemas (per schema: queries, mutations, types, HTTP middleware, and execution middleware) let us ship a public schema and an internal/admin schema from one codebase, with different middleware and different exposure. That directly enables a "partner API vs. internal tools" split without a second service.
- Feature scoping on performance. Two supported n+1 strategies mean performance work is a scoped, selectable investment rather than a rewrite:
- Dataloaders (built-in, uses
graphql-php deferred resolution) — works against any data source, best when fields fan out to services, caches, or non-Eloquent sources.
- SelectFields (
rebing/graphql-laravel-select-fields, optional separate package) — introspects the query to generate optimized Eloquent select() / eager-loaded with(). Best for classic relational CRUD.
We should budget one of these from day one; the "add it later" path is a real migration, not a config flip.
- Security and abuse posture as a launch gate. Depth limiting, query complexity analysis, batching limits, GET/read-only enforcement, and introspection off by default are shipped features, not bespoke work. This turns "is GraphQL a security liability?" from an open-ended objection into a checklist item we can close before GA.
- Observability as a supported feature. OpenTelemetry driver, per-field resolver tracing, custom tracing drivers, per-schema tracing — maps directly to existing SLO work rather than a parallel instrumentation project.
- Compliance / authorization decisions. Field-level
Privacy, per-operation authorize(), and resolver middleware mean record- and field-level access control (PII masking, tenant isolation, "hide this field from this role") is expressible declaratively. This is what makes a shared schema safe for multi-tenant or regulated data.
- Validation policy. Laravel validation rules attach directly to arguments (
rules() per field, per-argument rules, custom messages/attributes, cross-field rules in nested inputs). We can assert "same validation semantics as our REST endpoints" instead of re-litigating it.
- Testing strategy and CI cost.
httpGraphql() helper, assertSqlCount() / assertSqlQueries() for query-count regression tests, and documented patterns for testing mutations and non-default schemas mean query-count and authorization behavior are testable in CI. That's the mechanism that keeps performance and access control from silently regressing — worth calling out as a concrete decision enabler.
Decisions this does not support (and should be recorded as such): real-time/reactive product features. Subscriptions are explicitly unsupported; the README points to Lighthouse or a separate Laravel broadcasting/WebSocket implementation. If live updates are on the roadmap within ~12 months, either scope that as a separate track or pick differently now — retrofitting push onto a non-subscription GraphQL stack is a genuine architectural change.
When to Consider This Package
Adopt when most of these are true:
- Backend is Laravel 12.x–13.x on PHP 8.2+ and we're not planning a major framework move during the API's life. (This is a hard gate — the version floor is aggressive, and it's the main reason to look elsewhere.)
- The API's job is data retrieval and mutation for multiple heterogeneous clients where payload shape variance is a real cost today.
- The team is PHP-first and comfortable with a code-first schema — types, queries, mutations as classes, discovered and registered via
config/graphql.php or ConfigConvertible schema classes. No .graphql SDL files, no separate schema repo.
- We want GraphQL inside the monolith — no new service, no Node runtime, no additional deploy pipeline.
- We need multiple schemas (public + internal, or per-tenant/per-surface).
- We need strong authz granularity (field-level Privacy, per-operation authorize) and are willing to treat it as a design requirement, not an afterthought.
- We need a single API surface for internal admin tooling and external consumers and are willing to keep those schemas separate.
- We can commit to one data-loading strategy up front (Dataloader or SelectFields).
Look elsewhere — or reconsider the whole GraphQL decision — when:
- Subscriptions / real-time are in scope. Not supported. Evaluate Lighthouse (SDL-first, subscription support) instead. Note the tradeoff honestly: Lighthouse's SDL-first model is a different developer experience and arguably better for schema-governance-heavy orgs; this package's code-first model is better for Laravel-native teams that want PHP tooling, IDE navigation, and standard Laravel auth/validation in one place.
- We're on Laravel 11 or older, or PHP < 8.2. The version floor is unusually tight for a package with this adoption level. Confirm the support matrix before it becomes an upgrade blocker mid-project.
- We need SDL as the source of truth — schema registry, cross-team schema review, generated client types from a checked-in
.graphql file. SDL-first (Lighthouse, or a schema-first pipeline) is the better fit.
- The API is small, stable, and single-consumer. GraphQL's benefits evaporate; REST (or Laravel API Resources) is cheaper to build, cheaper to cache, and cheaper to explain. Don't adopt GraphQL to look modern.
- HTTP/CDN caching is a core requirement. GraphQL POST-heavy semantics complicate edge caching; if caching is central, weigh that against the client-flexibility win.
- We need first-class file uploads as a primary workflow. Supported, but it's a secondary path (documented Vue/vanilla examples); if uploads are a headline feature, validate it early with a spike.
- We can't staff ongoing schema stewardship. GraphQL moves complexity from the client to the server and from "many endpoints" to "one schema." Without someone owning deprecation (
field deprecation is supported), field hygiene, and complexity budgets, it degrades into an unqueryable blob.
Diligence items before committing (PM should ask for these in writing):
- Dependency-health check. ~2.2k GitHub stars with a recent release (2026-10-07) and CHANGELOG/releases discipline is a healthy signal, but the reported 0 dependents is anomalous for a package this widely used — likely a registry/indexing artifact. Verify Packagist download volume and reverse-dependency count directly, and confirm the maintainer bus factor. This is the single largest adoption risk: a MIT wrapper over
webonyx/graphql-php is replaceable in principle, but a mid-project abandonment still costs us a migration.
- Upstream coupling. Behavior is bounded by
webonyx/graphql-php ^15.22.1. Confirm the major-version upgrade path and how much of a webonyx breaking change leaks through this wrapper.
- Spike both data loaders on our real schema before committing — measure query counts with
assertSqlCount() on our most connected domain object — and pick one.
- Confirm security defaults in our config: introspection off in production, depth limit and complexity ceiling set, batch limits set, GET read-only enforced. Treat as a release checklist, not a code-review note.
- Decide the schema ownership model — who reviews schema changes, what counts as a breaking change, how deprecations are retired.
Rough effort framing for the decision doc: a 2–3 day spike (one type + query + mutation with auth, validation, and a data loader against a real Eloquent model) is enough to confirm the fit. Full adoption is not a large lift because the generators, middleware layers, and config scaffolds exist — which is itself the argument for buy-over-build here.
How to Pitch It (Stakeholders)
For executives — one paragraph:
We can expose a modern GraphQL API from our existing Laravel application without adding a service, a new language runtime, or a new on-call surface. rebing/graphql-laravel is a mature, MIT-licensed integration (2.2k GitHub stars, active releases, documented upgrade path) that supplies the undifferentiated plumbing — routing, validation, authorization, error formatting, rate/depth/complexity controls, tracing, and test helpers — so our engineers write only our domain schema. The business payoff is client flexibility: mobile, web, and partner consumers query exactly the fields they need, which reduces the versioning treadmill of per-consumer REST endpoints, and lets us ship the API domain-by-domain instead of as a big-bang migration. The cost is low and reversible: schema lives in our repo as plain PHP, so we retain ownership of the contract. Known limitation to accept explicitly: no GraphQL subscriptions, so real-time push would be a separate track — not a reason to delay this decision, but a reason not to promise live updates from this workstream.
For engineering leadership — the honest tradeoffs:
- What we're buying: routing, multi-schema support, two middleware layers (execution-level and per-resolver), argument validation wired to Laravel's validator, field-level
Privacy and per-operation authorize(), pagination, batching, unions/interfaces/input objects/OneOf inputs, APQ, OpenTelemetry tracing, query depth/complexity/batch limits, and 12 Artisan generators. None of that is interesting to build and all of it is easy to get subtly wrong — especially the security and error-formatting pieces.
- What we're owning: the schema, the data-loading strategy, the authorization model, and schema stewardship (deprecation discipline, complexity budgets). This is where the real work and the real long-term cost sit.
- The two things that will bite us if we skip them: (1) picking a data-loading strategy late — the n+1 problem in GraphQL is structural, and
assertSqlCount() in CI is how we keep it from regressing; (2) treating the security defaults as done-by-default — introspection is off out of the box (good), but depth, complexity, and batch limits need real numbers based on our schema.
- Platform constraints to confirm: PHP 8.2+ and Laravel 12–13 only. If we're mid-upgrade, sequence accordingly.
- The alternative we evaluated: Lighthouse (SDL-first, has subscriptions). We're choosing code-first because it keeps one language, one toolchain, and Laravel's native auth/validation — the right call unless real-time is near-term, in which case we should reopen this.
- Ask: approve a 2–3 day spike to validate the data-loader choice and the security config against a real domain object, then a phased rollout decision per domain.