Drupal's JSON:API in Core: Zero-Config Headless Backends Without Custom Controllers
Drupal's JSON:API module has shipped in core since 8.7, exposing every entity type as a spec-compliant RESTful API with filtering, pagination, sparse fieldsets, and compound documents — zero custom controllers required. Covers a fetch example, access control, cache tags, and production limits.
10 Oct 2026, 01:53 UTC

The Problem: Building Decoupled Drupal Without Writing Controllers
You need a RESTful API for a React, Vue, or Next.js frontend. Historically, this meant writing custom controllers, serializers, and routing — or installing a stack of contrib modules like REST UI, Serialization, and HAL. Each endpoint required configuration, permission mapping, and cache-tag wiring. The maintenance burden grows with every content type and relationship.
Since Drupal 8.7 (2019), the JSON:API module ships in core. It exposes every entity type — nodes, users, taxonomy terms, media, custom entities — at /jsonapi with zero code. The module follows the JSON:API 1.0 spec, so frontend teams get predictable compound documents, sparse fieldsets, filtering, sorting, and pagination out of the box.
What You Get Without Configuration
Enable the jsonapi module and visit /jsonapi. You'll see a top-level index linking to every entity type. Each resource collection (e.g., /jsonapi/node/article) supports:
- Compound documents:
?include=uid,field_tags,field_imagefetches related entities in one request. - Sparse fieldsets:
?fields[node--article]=title,created,field_tagsreturns only the fields you need. - Filtering:
?filter[status]=1&filter[type]=article(equality andINon base fields). - Sorting:
?sort=-created,title. - Pagination:
?page[limit]=25&page[offset]=0or cursor-basedpage[cursor].
All of this works because JSON:API uses Drupal's typed data and normalization pipeline. Computed fields, reference fields, and custom formatters that implement NormalizerInterface serialize automatically.
Worked Example: Fetching Published Articles with Tags and Author
Run this against a Drupal 10/11 site with the JSON:API module enabled (core, no extra install):
GET /jsonapi/node/article?filter[status]=1&sort=-created&page[limit]=5&include=uid,field_tags&fields[node--article]=title,created,field_tags&fields[user--user]=name,mail
Where to run: Any HTTP client (browser, curl, Postman, frontend fetch). No special permissions for published content.
Expected response shape: A JSON:API document with data array (articles), included array (user and taxonomy term objects), and links for pagination. Each article includes only title, created, and field_tags; each tag includes its default fields; the author includes name and mail.
Verification checks:
- Response has
200 OKandContent-Type: application/vnd.api+json. X-Drupal-Cache-Tagsheader contains tags likenode_list,config:jsonapi.settings.- Create an unpublished article — anonymous request returns 404 (or empty data); authenticated editor with "view unpublished content" permission sees it.
Access Control: It Uses Drupal's Entity Access System
No separate API permission layer. Anonymous users see only published content respecting node grants. Authenticated requests honor field-level permissions via hook_entity_access. If a role lacks "view author information" on user entities, the uid relationship returns null or omits the included resource. This means your existing content moderation workflows apply to the API automatically.
Performance and Cache Invalidation
Responses leverage Drupal's render cache and dynamic page cache. The X-Drupal-Cache-Tags header lists invalidation tags. For precise invalidation on relationship changes (e.g., a tag term updated), install the contrib module jsonapi_cache_tags — it adds cache tags for included relationships so a term save invalidates article collections that include it.
Default page[limit] is 50 with a hard cap of 1000 (configurable in jsonapi.settings configuration). Large exports should use cursor pagination or the Batch API instead of offset pagination.
Limitations You'll Hit in Production
- Filter syntax: Only simple equality and
INon base fields. ComplexWHEREclauses (range, LIKE, cross-field) need customQueryModifierplugins or thejsonapi_extrascontrib module. - Include depth: Unlimited by default. Deep includes on highly connected graphs cause N+1 queries. Set
jsonapi.max_include_depthin configuration or implement aQueryModifierto guard. - File/image derivatives: URLs point to the public file system. Private files return 403 in headless frontends unless you add a custom normalizer that generates signed URLs.
- Multilingual: Requires
Accept-Languageheader or?langcode=parameter. Missing language negotiation falls back to site default — SPA routers often forget to pass the header.
Schema Introspection for Frontend Teams
Frontend developers can generate TypeScript types or OpenAPI docs without Drupal knowledge. The top-level /jsonapi returns the index; /jsonapi/node/article returns the resource schema with attributes, relationships, and links. Tools like jsonapi-typescript or openapi-generator consume this directly.
Actionable Next Steps
- Enable
jsonapion a Drupal 10/11 site (core module, no Composer require). - Visit
/jsonapiand/jsonapi/node/articleto confirm the index and resource collection. - Test the worked example above; inspect
X-Drupal-Cache-Tags. - Create an unpublished node and verify anonymous 404 vs. editor 200.
- If you need deeper includes or complex filters, evaluate
jsonapi_extrasandjsonapi_cache_tagsbefore writing custom plugins.
JSON:API in core eliminates the boilerplate that used to make decoupled Drupal a project in itself. Start with the zero-config baseline; add contrib or custom plugins only when the spec's built-in capabilities reach their limits.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.