Drupal
How to ship Drupal JSON:API on production catalogues
Drupal JSON:API is the catalogue HTTP API I ship when a Drupal listing has to feed a separate front end, a native app, or a Magento or Shopify sibling that needs the same objects. I am Alan Vo, a Gold Coast web developer with 18 years on Drupal, Magento, Shopify, and WooCommerce catalogues. In 2026 this is still the production path for museum, regulator, property, and product listings that must leave the Twig page.
This note is for teams on Drupal 10 or 11 who already have content types, media, and access, and who were told to "just turn on JSON:API". Pair it with Drupal Search API on production catalogues when the public HTML search still needs facets, and with Drupal hreflang when the same objects exist in more than one language. The JSON:API module guide is the platform spec, and the JSON:API specification is the wire format. This note is how I run it on live catalogues, not a hello-world /jsonapi/node/article demo.
Why Drupal JSON:API matters on catalogues in 2026
Drupal JSON:API matters on catalogues in 2026 because it is the zero-configuration entity API that already respects Drupal access, already paginates, and already speaks a format non-Drupal clients understand. Core REST still exists. I do not use it for a property listing, an exhibition calendar, or a product family. The JSON:API versus core REST comparison is blunt: choose REST when you have non-entity data. Choose JSON:API for everything else. A Drupal catalogue is projects, exhibitions, providers, outdoor solutions, rooms, SKUs. JSON:API publishes a resource type per bundle. You do not hand-build a Views REST export for every filter. Magento storefronts I ship now often read Catalog Service GraphQL. On Drupal, this module is the equivalent job.
The 2026 ticket-changer is that Drupal CMS, Canvas, and decoupled marketing sites made "the React app will consume Drupal" a default brief again. Enabling the module without a surface area, a read-only lock, and an include budget is how unpublished nodes and original file URLs leak into a CDN.
How Drupal JSON:API actually works
Drupal JSON:API actually works as a resource type per entity bundle, under /jsonapi/{entity_type_id}/{bundle_id}, with an optional UUID for a single resource. The API overview is the map. Article nodes become node--article at /jsonapi/node/article. A project UUID is /jsonapi/node/project/{uuid}. Collections omit the UUID. Methods are GET, POST, PATCH, and DELETE. PUT is not in the spec Drupal implements.
Enable the core module. Then stop. Open /admin/config/services/jsonapi and leave read-only mode on unless the front end must create or update entities. Read-only is the default because most decoupled catalogues only read. Write mode turns every authenticated session into a CRUD client. I do not flip that for a brochure or a listing site.
Every GET I ship sends Accept: application/vnd.api+json. Collections use the query parameters the spec already named. Filter with conditions and groups; ?filter[status][value]=1 is the example the filtering guide gives. That filter is a performance hint, not access control. JSON:API already drops resources the user cannot view. Skip the status filter as anonymous and you still should not see drafts, but you will see fewer than page[limit] items because access stripped some of the queried set.
Sort with the short form sort=-created,uid.name or the expanded path and direction form on the sorting page. Include with ?include=field_image,uid so related objects land in included and the client does not N+1. Nested paths such as field_comments.uid are legal. The includes guide warns that a wide include on a collection is slower than two parallel requests. On a catalogue card I include the image and the term, not comments, revisions, and the author's picture on the same listing request. Sparse fieldsets keep the card small: fields[node--project]=title,field_status,field_city,field_image.
Pagination is page[limit] and page[offset]. Core caps the limit at 50. That is OffsetPage::SIZE_MAX in Drupal 11, documented on the pagination page as a DDoS guard: every extra row is an entity access check. Follow links.next. Do not invent the next URL. A page can return fewer than 50 even when more matching nodes exist. The client must keep following next until it is gone, not stop after one short page.
Language uses Drupal's negotiation. The translations page is honest about the limits: GET and includes work, PATCH can update a translation, DELETE removes the whole entity, and POST can create with a non-default langcode but cannot add a second translation. A bilingual museum client still needs a language prefix or header the site already negotiates.
JSON:API does not log users in. The what it does not do page sends you to /user/login?_format=json for cookies, /session/token for X-CSRF-Token on unsafe methods, and Simple OAuth when the client cannot hold a session cookie.
CORS is off until you enable it in services.yml. The decoupled getting started guide still shows cors.config.enabled: false. I set an explicit origin for the front-end host. I do not ship allowedOrigins: ['*'] with credentials.
Drupal JSON:API production checklist
A Drupal JSON:API production checklist starts with read-only mode, then the bundles you actually expose, then the includes and page size the front end is allowed to request.
- Enable core JSON:API. Confirm
/jsonapilists resource types. Confirm/admin/config/services/jsonapiis read-only unless write is a signed requirement. - Audit entity access and field access as anonymous, as an editor, and as a role that should not see drafts. JSON:API will not save you from a View unpublished content permission you granted too widely.
- Disable resource types you do not need. Core dispatches
ResourceTypeBuildEventso a custom module can calldisableResourceType()anddisableField(). JSON:API Extras 8.x-3.28 (19 December 2025, Drupal 11.3) gives a UI at/admin/config/services/jsonapito disable resources, rename fields, and set default includes. On a catalogue I disable user, comment, contact, and webform types the public client will never call. - Document the collection contract in the front-end repo: resource type, default filter (
status=1plus catalogue facets), default include, default sparse fieldset, and "always followlinks.next". - Keep
page[limit]at or under 50 unless you have a written reason. JSON:API Extras' Defaults submodule can raise it per resource. JSON:API Page Limit can raise it per path while leaving the default at 50. I would rather the client walknextthan ask for 1,000 projects in one hit. - Include only card fields on the listing request. Fetch the PDP as a single resource with a wider include.
- Add Consumer Image Styles when the front end needs derivatives. Register a consumer, pick styles, send
X-Consumer-ID. - Configure CORS with one origin and credentials only if cookies are the auth. Test a preflight from the real front-end host, not from curl on the Drupal box.
- Export config.
jsonapi.settings, Extras overrides, consumer config, and image styles belong in git. - Put HTML search on Search API plus Facets if merchandisers need a filter rail on the Drupal theme. JSON:API is the HTTP twin, not a replacement for that Views display.
- Load-test the collection URL with a realistic include as anonymous and as an editor. Watch PHP time and Redis, not only the 200.
What breaks on a live catalogue
What breaks on a live catalogue is almost never "JSON:API returned JSON". It is access, includes, pagination, and language.
Short pages: the app asked for 50, got 31, and assumed end of catalogue. Access removed 19. Follow next. Filter status=1 so unpublished nodes do not consume the 50-row budget. Wide includes on a 50-row collection (field_gallery, related nodes, author pictures) blow TTFB. Split the request. Original images at 4000px kill LCP; Consumer Image Styles exists so the API can hand back a derivative URL. CORS that "works in Postman" is not a browser test. enabled: false is still the default. Stay read-only until a ticket names the entity types the app must write. Translations follow Drupal negotiation, and DELETE removes the whole entity. Design language the same way you design hreflang on the HTML site. A leftover Views REST export beside JSON:API is two public APIs. Pick one.
How to measure Drupal JSON:API in production
How to measure Drupal JSON:API in production is to treat the collection URL as a store PLP: time, size, completeness, and access.
I keep a golden request per catalogue: resource type, filter, include, fieldset, page[limit]=50. I record TTFB, encoded bytes, data.length, whether links.next exists, and the X-Drupal-Dynamic-Cache / page-cache headers. After a content deploy I hit that URL as anonymous and as a logged-in editor. If anonymous data.length jumps, access slipped. If TTFB doubles, someone widened include.
On the client I log how many next hops a full listing crawl takes, and whether any hop returns fewer items than limit while next is still present. That is the access-strip pattern. CDN cache keys must include the query string and X-Consumer-ID if you use it. A cached English collection served to a French Accept-Language is a language bug, not a JSON:API bug. I measure whether the front end rendered the same published set Drupal's HTML listing shows, and whether a save in Drupal invalidates that page inside the cache budget we agreed.
Related work on this site
Related work on this site is Drupal catalogues that already think in listings, not in a single landing page. The Immobel Group site is Drupal for a listed European developer: projects with location, type, and status, the same problem as a store PLP even though the items are developments. The Royal Ontario Museum rebuild is bilingual Drupal for exhibitions, membership, and visits, so any JSON:API collection has to negotiate language the same way hreflang does. IASO is Drupal for outdoor products, where a product family is a bundle with photography. All three were contract work on an agency team. None of them should expose every core resource type because a front-end repo asked for "the API".
FAQ
When do I enable Drupal JSON:API on a catalogue?
You enable Drupal JSON:API on a catalogue when a non-Twig client must read those entities: a separate front end, a native app, or another store that consumes the same objects. You do not enable it because a checklist said "headless". If editors only use Drupal pages, leave the module off.
Does Drupal JSON:API replace Search API or Views?
Drupal JSON:API does not replace Search API or Views. Collections are the API-first stand-in for a Views REST export, with filters and pagination built in. Faceted HTML search, Solr, and merchandiser rails still belong on Search API. I often ship both: JSON:API for the app, Search API for the Drupal theme.
Can Drupal JSON:API return unpublished catalogue items?
Drupal JSON:API can return unpublished catalogue items to any user whose Drupal permissions already allow that view. Read-only mode does not hide them. Filter status=1 to keep those rows out of the query. Fix the role if anonymous or the app user can see drafts in the HTML site too.
Why is my Drupal JSON:API page smaller than page[limit]?
Your Drupal JSON:API page is smaller than page[limit] because the module queries that many entities, then drops the ones the current user cannot access. Follow links.next. Add a status filter so unpublished nodes do not consume the 50-row budget. Do not raise SIZE_MAX to paper over an access leak.
Keep reading
Contact if you want this kind of work on a live store.