Je hebt Javascript nodig om deze website te kunnen gebruiken. Pas je browserinstellingen in om verder te gaan!
Directus

Advanced usage

Adapt production Directus query patterns for FAQs, articles, and institutions.

These examples recreate API calls used on onderwijsloket.com. Adapt the requested fields and filters to your UI; the complete snippets also live in our code snippets repository.

Fetch filtered FAQs

Suppose you are building an application that only contains content related to primary and secondary education. You want to create a landing page for the topic “salary” and include a relevant FAQ section.

You can achieve this by applying the appropriate filters to your API query.

In the example below:

  • We assume there are sector items named “voortgezet onderwijs” and “primair onderwijs”.
  • We use the _icontains operator to ensure case-insensitive filtering.
  • We assume there is a topic containing the word “salaris”.
  • We filter FAQs linked to that topic and either of the two sectors.
// Get your client
const directus = useDirectus();

const data = await directus(
  readItems("faqs", {
    fields: ["id", "question", "answer"],
    filter: {
      _or: [
        {
          sectors: {
            sectors_id: {
              title: {
                _icontains: "primair onderwijs"
              }
            }
          }
        },
        {
          sectors: {
            sectors_id: {
              title: {
                _icontains: "voortgezet onderwijs"
              }
            }
          }
        }
      ],
      topics: {
        topics_id: {
          name: {
            _icontains: "salaris"
          }
        }
      }
    }
  })
);
You do not have to use _icontains for this query. If you know the IDs of the relevant topics and sectors, you can use _eq or _in to filter directly by ID instead.

Fetch an article by slug

If you want to implement articles from Onderwijsloket in your own knowledge base, you need to fetch and render the relevant data—similar to how we do it internally.

In this example, we fetch an article by its slug field using readItems. We also:

  • Fetch relational data used by the editor
  • Inject relational data into the content document
  • Remove junction tables from the response

Request the article and its dependencies

import type {
  ArticlesEditorNode,
  Author,
  Topic,
  Sector,
  Role,
  Track,
  Qualification,
  Document,
  Source,
  DirectusFile
} from "@your-generated-schema";

import { injectData, editorNodeFields } from "your-content-renderer-helpers";

// Get your Directus client
const directus = useDirectus();

// Get the slug from the route
const slug = getSlugFromRoute();

const data = await directus(
  readItems("articles", {
    filter: {
      // Filter results based on slug value
      slug: { _eq: slug }
    },
    fields: [
      /**
       * Top-level article fields
       */
      "title",
      "id",
      "slug",
      "path",
      "description",
      "summary",
      "body",
      "date_created",
      "date_updated",
      "seo",
      "english",
      {
        /**
         * Relational data used by the editor
         * @see https://docs.onderwijsloket.com/misc/content-documents
         */
        editor_nodes: editorNodeFields
      },

      /**
       * Additional relational data
       */
      {
        authors: [
          {
            authors_id: ["first_name", "name", "image", "job_title"]
          }
        ]
      },
      {
        documents: [
          {
            documents_id: [
              "id",
              "title",
              "description",
              {
                asset: ["id", "filesize", "type"]
              }
            ]
          }
        ]
      },
      {
        sources: [
          {
            sources_id: ["id", "title", "description", "url"]
          }
        ]
      },
      {
        topics: [
          {
            topics_id: ["id", "name", "path"]
          }
        ]
      },
      {
        sectors: [
          {
            sectors_id: ["id", "title", "slug"]
          }
        ]
      },
      {
        roles: [
          {
            roles_id: ["id", "name", "path"]
          }
        ]
      },
      {
        tracks: [
          {
            tracks_id: ["id", "name", "path"]
          }
        ]
      },
      {
        qualifications: [
          {
            qualifications_id: ["id", "name", "path"]
          }
        ]
      }
    ]
  })
);

// Since slug values are unique, we can assume the first item is the correct article.
const item = data[0];

if (!item) {
  // Handle missing article (e.g. return 404)
  return handle404();
}

Inject relational data into the content document

/**
 * Inject editor nodes into the body content
 * @see https://docs.onderwijsloket.com/misc/content-documents#injecting-relational-data-into-the-json-document
 */
const bodyWithInjectedData = injectData(item.editor_nodes as ArticlesEditorNode[], item.body);

For more details on content documents and injecting relational data, refer to the related guide.

Content Documents
Learn how to work with our rich JSON documents and the relational nodes.

Remove junction tables from relationships

The helper function below extracts nested relational data from the Directus response and maps it back to a clean structure without junction tables.

function getNestedRelationships<T>(item: unknown, config: { key: string }): T[] {
  const relation = (item as Record<string, unknown>)[config.key] as unknown[];

  if (!Array.isArray(relation)) {
    return [];
  }

  return relation.map((rel) => {
    const idKey = `${config.key}_id`;
    const id = (rel as Record<string, unknown>)[idKey];

    if (typeof id === "object" && id !== null && "id" in id) {
      return { ...(rel as object), ...id } as T;
    }

    return rel as T;
  });
}

You can then construct the processed article object:

const processedItem = {
  ...itemWithRelationalData,
  authors: getNestedRelationships<Partial<Author & { image: string }>>(item, {
    key: "authors"
  }),
  topics: getNestedRelationships<Partial<Topic>>(item, { key: "topics" }),
  sectors: getNestedRelationships<Partial<Sector>>(item, { key: "sectors" }),
  roles: getNestedRelationships<Partial<Role>>(item, { key: "roles" }),
  tracks: getNestedRelationships<Partial<Track>>(item, { key: "tracks" }),
  qualifications: getNestedRelationships<Partial<Qualification>>(item, {
    key: "qualifications"
  }),
  documents: getNestedRelationships<Document & { asset: Partial<DirectusFile> }>(item, {
    key: "documents"
  }),
  sources: getNestedRelationships<Partial<Source>>(item, {
    key: "sources"
  })
};

At this point, processedItem contains:

  • Injected editor content
  • Clean relational data without junction tables
  • A structure ready for rendering in your frontend application

Fetch an institution and its programmes

Suppose you want to create a details page for a specific educational institution. For example, consider Vrije Universiteit Amsterdam.

There are several ways to retrieve this item depending on what information you already have:

  • ID known → Use readItem
  • Slug known → Use a filter on slug
  • Only the title known → Use _icontains (e.g. { "title": { "_icontains": "vrije universiteit" } })

The last option is less reliable because titles can change or vary in formatting. Therefore, if the ID is known, it is the most straightforward and robust approach.

In this example, we will fetch the institution using its ID:

c23d4070-1c5c-5ec8-a182-8a2e1c313ca9

Select only the fields you render

To retrieve all relevant information about the institution and its programs, we need to include nested relational data.

In this example, we fetch data two levels deep:

  1. Programs related to the institution
  2. Qualifications related to each program
Fetching relational data requires traversing junction tables when dealing with many-to-many relationships.

The field selection below includes:

  • Top-level institution fields
  • The description rich text document
  • Related programs
  • Related qualifications within each program
import type { EducationalInstitution, Schema } from "@schema";

import type { QueryFields } from "@directus/sdk";

/**
 * Field selection for the query.
 */
const fields: QueryFields<Schema, EducationalInstitution> = [
  /**
   * Top-level fields:
   * Include any fields from the educational_institutions collection that you need in your application
   */
  "id",
  "title",
  "slug",
  "path",
  "date_created",
  "date_updated",

  /**
   * The tiptap document of Educational Institutions do not contain relational nodes like in Articles.
   * That means fetching the `description` field will return all relevant data.
   */
  "description",

  /**
   * Embed related programs using Directus' nested read functionality.
   * Programs <> Educational Institutions is a many-to-many relationship, so we need to traverse the
   * junction table (educational_institutions_programs) to get the related programs.
   */
  {
    programs: [
      {
        programs_id: [
          /**
           * Select fields to include from the related programs. If needed, you can even
           * embed deeper relationships using the same syntax.
           */
          "id",
          "title",
          "slug",
          "level",
          {
            /**
             * Embed qualifications related to the program. This is also a many-to-many relationship,
             * so we need to traverse the junction table (programs_qualifications) to get the related qualifications.
             */
            qualifications: [
              {
                qualifications_id: ["id", "name", "description"]
              }
            ]
          }
        ]
      }
    ]
  }
];

Construct the request

Since we know the institution ID, we can use the readItem request. In addition to the field selection, we include a deep** parameter** to control how related data is fetched.

By default, Directus limits relational queries to 100 items.Setting _limit: -1 disables pagination and ensures all related programs are returned.

The example below also demonstrates filtering related programs so that only master-level programs are returned.

const ID = "c23d4070-1c5c-5ec8-a182-8a2e1c313ca9";
import { directus } from "./client";

const item = await directus.request(
  readItem("educational_institutions", ID, {
    fields,

    /**
     * We want to make sure we fetch ALL related programs, so we need to set a limit via the `deep` parameter.
     * By default the query limit is set to 100 (which is sufficient to fetch all programs, but for the sake of
     * this demo, we will increase it anyway).
     */
    deep: {
      programs: {
        _limit: -1, // Set to -1 to disable pagination and fetch all related items

        /**
         * You can optionally set other 'deep parameters' such as _sort or _filter to further customize the query for related items.
         * For more info @see https://directus.io/docs/guides/connect/query-parameters#deep
         */
        _filter: {
          // For example, only fetch master level programs
          // Here we need to traverse the junction table as well
          programs_id: {
            level: {
              _eq: "master"
            }
          }
        }
      }
    }
  })
);

Flatten junction records

The response contains two junction layers:

  • eductional_institutions_programs
  • programs_qualification

Since junction records do not contain meaningful data themselves, it is often helpful to flatten them before using the data in your application.

import { getNestedRelationships } from "./get-nested-relationship";

/**
 * Flatten junction tables two levels deep (programs and qualifications)
 */
const processedItem = {
  ...item,
  programs: getNestedRelationships<Partial<Program>>(item, {
    key: "programs"
  }).map((program) => ({
    ...program,
    qualifications: getNestedRelationships<Partial<Qualification>>(program, {
      key: "qualifications"
    })
  }))
};

This transformation removes unnecessary junction layers, allowing your application code to work directly with:

  • programs
  • qualifications

Keep in touch with the latest

Sign up for our monthly deep dives - straight to your inbox.