Drupalwoo ☉

Expose custom Drupal content through JSON:API

Drupal’s JSON:API module exposes Drupal entities through a standards-based HTTP interface. Responses follow the JSON:API format, so clients can work with predictable data, relationships, included, and links members rather than a bespoke endpoint invented for one site. It suits decoupled front ends, mobile applications, integrations, and progressive enhancement.

Drupal content modelling still determines what can be exposed. A custom node type named property_development, for example, becomes available under /jsonapi/node/property_development once JSON:API is enabled and the request passes entity access checks. Site builders can shape useful APIs without writing a controller for every collection or record.

The module supports reads and writes for eligible resources, including nodes, users, taxonomy terms, and media. It does not expose every configuration object or arbitrary internal value. The practical task is to design fields deliberately, grant precise permissions, and decide how relationships, pagination, and errors should behave for the consumers who will use the API.

Model content around stable data

Create the content type and its fields before connecting a client. Use clear machine names such as field_price, field_bedrooms, and field_status; these become part of the client contract. Give each field a sensible widget and help text for editors, while keeping the stored value independent of the label shown on a page.

Choose a field type that matches the business value. Store a property price as an integer or decimal number of Australian dollars, not as a formatted string such as $780,000. Use a date or timestamp field for an inspection appointment and a boolean where the only values are true and false. Entity references are appropriate for agents, offices, and media. This matters for a Sydney property portal: consumers can sort numerically, format the amount as AUD, and schedule an inspection without parsing Australian display text.

The resource type identifies the content-type bundle, and the entity UUID identifies one record. A compact list response can therefore look like this:

{
  "data": [
    {
      "type": "node--property_development",
      "id": "6c46b0e4-4f12-4fb8-8c63-e830df921d42",
      "attributes": {
        "title": "Two-bedroom apartment in Parramatta",
        "field_price": 780000,
        "field_bedrooms": 2,
        "field_status": "available"
      },
      "relationships": {
        "field_agent": {
          "data": {
            "type": "user--user",
            "id": "17"
          }
        }
      }
    }
  ]
}

The UUID remains stable when a title or price changes, giving clients a dependable key for caching and updates. It also means that changing a field label is harmless, while renaming a field machine name is a breaking API change.

Install JSON:API and configure access

Install the contrib module with Composer, then enable it with Drush:

composer require drupal/jsonapi
drush en jsonapi -y

The precise commands can vary by Drupal version and deployment setup, but the endpoint follows a predictable structure: /jsonapi/{entity-type}/{bundle}. JSON:API works with normal Drupal routing and access handling; it is not a separately authenticated copy of the site that ignores entity permissions.

Create a dedicated API role rather than granting broad administrator access. Readers need the JSON:API route permission and the appropriate view permission for the selected content types. Writers need create, update, and delete permissions for those bundles, plus access to referenced records. Public access can be useful for published listings, while unpublished records and contact details should remain behind authenticated roles. Permission changes should be tested with an account that has no unrelated administrative rights.

Serve the endpoint over TLS, ideally on a dedicated host such as api.example.com.au that matches the organisation’s Australian domain and DNS arrangement. Configure CORS for the exact front-end origins that need access, including the scheme, rather than reflecting every requesting origin. OAuth 2, a gateway-issued token, or a configured Basic HTTP Authentication provider can protect write endpoints, but the choice depends on the hosting environment. Never place a production credential in browser JavaScript.

Read content with useful queries

A collection request needs the correct resource type, bundle, and media type. This example returns available properties and asks the server for ten records:

curl --get 'https://www.example.com.au/jsonapi/node/property_development' \
  -H 'Accept: application/vnd.api+json' \
  --data-urlencode 'filter[field_status][condition][path]=field_status' \
  --data-urlencode 'filter[field_status][condition][operator]=' \
  --data-urlencode 'filter[field_status][condition][value]=available' \
  --data-urlencode 'sort=-field_price' \
  --data-urlencode 'page[limit]=10'

The filter uses the field machine name, not its editor-facing label. JSON:API query operators can express equality, comparisons, ranges, and membership, so a consumer can request properties within a price band without retrieving the entire collection. Sorting uses the exposed field name, while page[offset] and page[limit] provide offset pagination. The server’s configured page-size limit still applies.

Sparse fieldsets reduce unnecessary work and transfer. For example, fields[node]=title,field_price,field_bedrooms asks for three node attributes, while fields[user]=display_name limits fields on an included user. Combine this with include only when the client actually needs a relationship. A list endpoint returns a data array; a single-resource endpoint can return one object. Query errors and access failures use the JSON:API errors collection, giving client software a consistent structure to handle.

Use Accept: application/vnd.api+json for compatibility with clients that negotiate the media type. Check the response status as well as the body, because standard 4xx and 5xx statuses remain essential for monitoring. Cacheable GET requests are easy to integrate with a CDN or reverse proxy, provided the cache key includes every filter, sort, and page parameter.

Include relationships without bloating responses

Entity references appear as relationship objects in each resource. Add include=field_agent to include the referenced user, or request several relationships as a comma-separated list. The original resource still contains the relationship linkage, while matching related records appear in the top-level included array.

A relationship with one target contains a data object; a multi-value relationship contains a data array. This distinction allows a client to render either one agent or a list of contributors without reverse-engineering the response. Media relationships require extra care: confirm that the returned media includes a usable image URL, alternative text, and appropriate access rules rather than assuming every file is public. An include request never overrides Drupal’s entity access checks.

Payload discipline is valuable for teams sending field data from Perth and other regional areas over mobile connections. Limit fields, paginate early, and cache stable reads on the device. Cache entries by resource UUID and query parameters, then refresh only the records that changed. Sparse fieldsets, conditional requests, and a deliberate cache policy are especially useful for a field-inspection application operating across variable mobile coverage.

Create and update records safely

Write operations use the same resource paths and JSON:API document structure. The following request creates a property and assigns an agent; it assumes an OAuth 2 provider has issued $ACCESS_TOKEN and that the editor role can create this bundle.

curl -X POST 'https://api.example.com.au/jsonapi/node/property_development' \
  -H 'Accept: application/vnd.api+json' \
  -H 'Content-Type: application/vnd.api+json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data '{
    "data": {
      "type": "node--property_development",
      "attributes": {
        "title": "Warehouse studio in Footscray",
        "field_price": 525000,
        "field_bedrooms": 1,
        "field_status": "available"
      },
      "relationships": {
        "field_agent": {
          "data": {
            "type": "user--user",
            "id": "17"
          }
        }
      }
    }
  }'

Do not send id when creating a resource because Drupal generates the UUID. For an update, send the resource type, UUID, and changed attributes to the item endpoint:

{
  "data": {
    "type": "node--property_development",
    "id": "6c46b0e4-4f12-4fb8-8c63-e830df921d42",
    "attributes": {
      "field_status": "under-contract"
    }
  }
}

Normal field validation, entity access checks, and the standard save pipeline still apply. A client cannot publish content merely because it knows the endpoint. Editing permissions, workflow requirements, and validation errors must all be respected. Use a dedicated service account with only the required bundle permissions, and rotate its credential through the hosting platform’s secret store rather than committing it to a repository.

Drupal also fires the usual entity events when JSON:API creates or updates a node. If a module needs to react to those saves, the Symfony event dispatcher pattern connects node hooks with Symfony events without requiring the API client to call a custom callback. This keeps side effects such as notifications or indexing inside Drupal’s normal extension model.

Operate the endpoint in production

Treat public reads, authenticated reads, and writes as different traffic classes. Anonymous GET responses can often be cached at the edge, but responses containing unpublished material or user-specific fields should not enter a shared cache. Include the complete query string in cache keys, and prevent authenticated responses from being stored as public variants. Apply sensible rate limits at the firewall or API gateway, and use pagination to protect the origin from oversized collection requests.

Monitor status codes, response time, payload size, and error types. Keep access tokens and personal fields out of routine application logs. The privacy implications are concrete for Australian businesses: the Privacy Act 1988 and Australian Privacy Principles may apply to agent names, contact details, and behavioural data. Define what the API exposes, who can read it, how long it is retained, and whether consent is required. Least-privilege roles and field-level access are part of the API design, not tasks to add after launch.

Treat field machine names, resource types, and UUIDs as a published contract. Avoid renaming them casually; introduce a new content-type bundle or a purpose-built versioned endpoint when a breaking change is unavoidable. Dates and times should be exchanged in ISO 8601 with an explicit offset, while the interface can render familiar day-first DD/MM/YYYY values. A Melbourne venue must account for daylight-saving changes under Australia/Melbourne, and similarly distinct zones apply elsewhere. Stable values, precise permissions, and disciplined caching let the API remain reliable across the Australian market.