Advanced usage

Discover advanced features of the Directus SDK through real-world examples.

In this article, we recreate the API calls used in our own frontend at onderwijsloket.com. All examples and code snippets can also be found in our code snippet repository.

Fetching 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.

Fetching 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

Fetch the Article

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/guides/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/guides/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

Fetching an Educational Institution and Its Programs

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

Field Selection

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'],
							}
						]
					}
				]
			}
		]
	}
]

Constructing 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',
						}
					}
				}
			}
		}
	}),
)

Flattening 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