Skip to content

Goal: list the published entries of one content type on your site from your terminal.

  • The machine name of a content type, read from your site’s JSON:API Query Builder.
  • One successful GET /api/node/{type} request and a map of where entries, IDs, and fields live in the response.
  • Proof that query parameters work, by limiting the result with page[limit].
  • The API client from the auth quickstart: you’ll reuse its .env file.
  • Node.js 20.6 or later for the JSON:API Client and JavaScript paths, or curl (preinstalled on macOS and Linux).
  1. In your site’s left sidebar, go to API > JSON:API Query Builder. In the Entity Type dropdown select the content entity type (node), then pick your content type in the Bundle dropdown.

    The Request URL section now shows the exact URL for that type. The last path segment is the machine name:

    https://your-site.example.com/api/node/article

    The machine name (article here) is what the API uses. It is lowercase with underscores, and often differs from the label editors see: a type labeled “News post” might be news_post. The steps below use article; substitute yours.

    No browser handy? The API root you called at the end of the auth quickstart already lists every type: look for the links entries starting node--: the part after the double dash is the machine name.

  2. Call the collection endpoint with the credentials from the auth quickstart’s .env:

    The Drupal API Client addresses the type by its resource type string: entity type and machine name joined with a double dash, node--article (the same format the response’s type member uses). In a directory with the .env file:

    Terminal window
    npm install @drupal-api-client/json-api-client
    list-articles.mjs
    import {
    JsonApiClient,
    } from "@drupal-api-client/json-api-client";
    const client = new JsonApiClient(
    process.env.DRUPAL_SITE_URL,
    {
    // Source CMS serves JSON:API at /api
    apiPrefix: "api",
    authentication: {
    type: "OAuth",
    credentials: {
    grantType: "client_credentials",
    clientId: process.env.DRUPAL_CLIENT_ID,
    clientSecret: process.env.DRUPAL_CLIENT_SECRET,
    },
    },
    },
    );
    const articles = await client.getCollection(
    "node--article",
    );
    console.log(JSON.stringify(articles, null, 2));
    Terminal window
    node --env-file=.env list-articles.mjs

    No token handling: the client exchanges the credentials for a token itself and refreshes it when it expires.

    A 200 response returns the first page of published entries of that type, up to the page[limit] default of 50. Beyond that, follow links.next. (The API sends one compact line; shown formatted and trimmed here.)

    {
    "jsonapi": {
    "version": "1.1",
    "meta": {
    "links": {
    "self": {
    "href": "http://jsonapi.org/format/1.1/"
    }
    }
    }
    },
    "data": [
    {
    "type": "node--article",
    "id": "3f9a2b1e-6c4d-4a8e-9b7f-1d2e3c4b5a69",
    "links": {
    "self": {
    "href": "https://your-site.example.com/api/node/article/3f9a2b1e-6c4d-4a8e-9b7f-1d2e3c4b5a69?resourceVersion=id%3A1"
    }
    },
    "attributes": {
    "langcode": "en",
    "status": true,
    "title": "Spring launch recap",
    "created": "2026-05-11T09:30:52+00:00",
    "changed": "2026-05-12T14:02:10+00:00",
    "path": {
    "alias": null,
    "pid": null,
    "langcode": "en"
    }
    },
    "relationships": {
    "node_type": { "…": "…" },
    "uid": { "…": "…" }
    }
    },
    {
    "type": "node--article",
    "id": "…more entries…",
    "attributes": { "…": "…" }
    }
    ],
    "meta": { "count": 5 },
    "links": { "self": {
    "href": "https://your-site.example.com/api/node/article"
    } }
    }

    Where to look:

    • data: the entries. One object per entry.
    • type: entity type plus bundle machine name, joined with a double dash: node--article.
    • id: the entry’s UUID, its stable identifier. One entry lives at /api/node/article/{id}.
    • attributes: the entry’s field values, keyed by field machine name. title, created, changed, status, langcode, and path appear on every content type; your type’s own fields appear alongside them, so your list will be longer and different.
    • relationships: pointers to related entries and metadata. Every type has node_type and uid; reference fields your type defines (media, taxonomy terms) appear here too. The querying guide shows how to pull them into the same response.
  3. Add one query parameter:

    Query parameters are built with the drupal-jsonapi-params package, the client’s companion for everything you’d otherwise hand-write into the URL:

    Terminal window
    npm install drupal-jsonapi-params
    // add to list-articles.mjs
    import {
    DrupalJsonApiParams,
    } from "drupal-jsonapi-params";
    const queryString = new DrupalJsonApiParams()
    .addPageLimit(2)
    .getQueryString();
    const limited = await client.getCollection(
    "node--article",
    { queryString },
    );
    console.log(JSON.stringify(limited, null, 2));

    getQueryString() produces page[limit]=2 (URL-encoded), so you never escape brackets yourself.

    data now holds at most 2 entries, and when more exist, links gains ready-made next and last URLs:

    {
    "data": [
    { "type": "node--article", "…": "…" },
    { "type": "node--article", "…": "…" }
    ],
    "links": {
    "last": {
    "href": "https://your-site.example.com/api/node/article?page%5Boffset%5D=4&page%5Blimit%5D=2"
    },
    "next": {
    "href": "https://your-site.example.com/api/node/article?page%5Boffset%5D=2&page%5Blimit%5D=2"
    },
    "self": {
    "href": "https://your-site.example.com/api/node/article?page%5Blimit%5D=2"
    }
    }
    }

    That’s the whole mechanism: everything you will ever ask the Content API for (filters, fields, related entries, sorting) is a query parameter on a GET, shaping the same response you just read.

GET /api/node/{type} is the collection endpoint for one content type, and query parameters shape what it returns. Your site ships two companion tools for going further. The JSON:API Query Builder (API > JSON:API Query Builder) builds these queries visually (fields, filters, includes, sort), previews live responses, and generates code samples. The per-site OpenAPI documentation (API > OpenAPI documentation) lists every endpoint your site exposes, with interactive try-it-out calls.

"data": [] with 200 OK

the request worked but returned nothing. Three distinct causes:

  • You queried the wrong type. The URL names a real type, but not the one your entries belong to (for example node/page while your content is node/article). Check: re-select the entity type and bundle in the Query Builder and compare its Request URL with yours; remember it’s the machine name, not the label, and it’s case-sensitive lowercase.
  • The type has no published content. The name is right but nothing is published. Check: open your site’s content list, filter to that type, and confirm at least one entry is published.
  • The entries exist but you’re not allowed to see them. When data is empty (or shorter than the response’s meta.count) and the response carries a meta.omitted block ("detail": "Some resources have been omitted because of insufficient authorization."), the entries are unpublished or access-restricted, not missing. Check meta.omitted before concluding there’s no content. Entries created via the API are unpublished by default. See the writing content guide.

404 Not Found

the type in the URL doesn’t exist at all: a label used in place of the machine name (node/Article, node/news post), wrong case, or a typo. Copy the URL from the Query Builder’s Request URL section instead of typing it.

401 Unauthorized

the token is missing or expired (tokens expire after expires_in seconds: 300, so 5 minutes). In the JavaScript path, rerun the script for a fresh token; in the curl path, re-run the TOKEN= command from step 2 in the same shell. The JSON:API Client refreshes tokens itself, so a 401 there points at the credentials. More cases in the auth guide’s troubleshooting.

SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON

from the JSON:API Client: the client called its default base path /jsonapi and got an HTML page back. Source CMS serves the API at /api; construct the client with apiPrefix: "api", as in step 2.

curl: (3) bad range in URL position …

curl interpreted the [ ] in page[limit] as glob syntax. Add -g (globoff), as in step 3. Note that -s silences this error entirely: if a bracketed request prints nothing at all, rerun it without -s to see what curl is complaining about.

  • First fetch: turn this call into your first rendered page.
  • Querying guide: filters, sparse fieldsets, related entries, pagination, and sorting, composed into real queries.
  • API > JSON:API Query Builder on your site: build and preview any query visually, then copy the URL or generated code.
  • API > OpenAPI documentation on your site: the interactive list of every endpoint your site exposes.

Was this page helpful?