Directus Data Layer Patterns
Directus is a self-hosted, database-backed headless CMS that exposes your relational tables through REST and GraphQL. Treating it as a structured data layer rather than a traditional CMS gives you predictable query patterns, explicit schema control, and framework-agnostic integration. This guide covers four production patterns — normalized modeling, query projection, caching, and custom endpoints — within Platform Integration Deep Dives.
The four patterns stack from the database up to the frontend, each constraining what the next layer sees:
Integration Contract
A Directus integration rests on a few explicit agreements. Model: collections map to SQL tables, changed through schema snapshots applied by CI, never by hand in production. Access: the public role or a dedicated read-only token can read published items only, filtered by a status field; preview access uses a separate token scoped to versions or drafts. Events: a flow sends signed requests to the frontend’s revalidation endpoint on create, update and delete of routable collections. Fetching: one data layer builds queries with explicit fields and filters, and components never see raw Directus responses. Assets: images are served through Directus’s asset transformations or a CDN, with dimensions stored on each file.
# .env: Directus integration
DIRECTUS_URL=https://cms.example.com
DIRECTUS_READ_TOKEN=static_token_for_read_only_policy
DIRECTUS_PREVIEW_TOKEN=static_token_for_preview_policy
DIRECTUS_WEBHOOK_SECRET=shared_secret_for_flow_requests
Pattern 1: Normalized Schema & Relational Modeling
Directus maps collections straight to SQL tables, so it performs best when the content model is normalized rather than deeply nested JSON. Prefer flat schemas with explicit O2M and M2M junction tables, and enforce NOT NULL, defaults, and strict types at the database level so the frontend never dereferences a null.
Model reusable components as separate collections, not embedded JSON blobs — that buys cross-collection querying, consistent validation, and cleaner migrations. The contrast with managed platforms is concrete: the Contentful Integration Guide hides relationships in a proprietary graph, while Directus keeps foreign keys transparent and queryable in plain SQL. Use database constraints for referential integrity and the relational field UI for consistency, no custom validation scripts.
Pattern 2: Query Projection & Database-Level Filtering
Don’t fetch full relational trees and filter in the browser. Directus does field projection, deep filtering, and aggregation server-side — restrict payloads with fields and push predicates to the database with filter operators.
Framework-agnostic HTTP pattern:
GET /items/articles?fields=id,title,slug,author.name,category.slug&filter[status][_eq]=published&filter[date_published][_lte]=$NOW&sort=-date_published&limit=20
This reduces network overhead, minimizes client-side transformation, and allows frontend frameworks to consume responses directly. Nested fields are selected with dot notation in fields; the deep parameter applies filters, sorting and limits to nested relations, such as returning only an article’s three most recent approved comments:
{
"fields": ["id", "title", "comments.id", "comments.body", "comments.date_created"],
"deep": {
"comments": { "_filter": { "status": { "_eq": "approved" } }, "_sort": ["-date_created"], "_limit": 3 }
}
}
For preview, Directus offers content versioning: editors work in a version of an item and share a preview URL configured on the collection, while the frontend fetches that version with a server-side token, as described in Directus content versioning for draft previews. Keep tokens out of preview URLs; pass a short-lived signed preview secret instead. Align query contracts with the OpenAPI Specification for type-safe client generation and predictable payloads.
Pattern 3: Caching, ISR, and Build-Time Hydration
Directus serves Cache-Control headers and ETag validation out of the box. For Jamstack, drive ISR from Directus webhooks into framework revalidation endpoints, and tag the cache by collection name so a publish purges only what changed. At build time, fetch only what the initial route needs via limit/offset; defer heavy relational queries to hydration or edge middleware, and layer stale-while-revalidate to hold sub-second TTFB without layout shift. The Directus as a headless data layer for Jamstack blueprint has the cache topology; Next.js ISR docs cover aligning revalidation intervals to your publishing cadence.
Pattern 4: Extending the Data Layer with Custom Endpoints
When CRUD isn’t enough, register custom routes and GraphQL resolvers in-project to encapsulate business logic, aggregate cross-collection metrics, or proxy third-party APIs without leaking credentials to the client. Validate input before execution and attach rate-limiting middleware. See Directus custom API extensions for frontend apps for the permission-aware implementation.
Directus pays off when treated as a transparent SQL-backed data layer: normalize schemas, project queries server-side, cache deterministically, and extend the API only when standard operations fall short.
Page Builders with Many-to-Any Relations
Directus supports page-builder models through many-to-any relations: a page has a blocks field that links to items in several block collections, such as block_hero, block_text and block_gallery, each with its own fields. The API returns each block with its collection name, which serves as the discriminator for rendering. Request block fields per collection with the fields syntax for many-to-any, for example blocks.item:block_hero.headline, so each block type returns only its own fields. On the frontend, validate the block array with a discriminated union keyed on the collection name and render through a typed registry, as described in modeling page-builder blocks. Keep the junction table’s sort field in the query, so blocks render in the order editors arranged them.
Localization with Translations Collections
Directus localizes through a translations interface backed by a junction collection per translated collection, such as articles_translations, linked to a languages collection. Each translation row holds the localized fields for one language. Query the translation for the requested language with a deep filter on the translations relation, and fall back in code when it is missing, recording which language was served for notices and hreflang, as described in content fallback routing. Slugs can live in the translations table for translated URLs; enforce uniqueness per language with a database constraint on the junction table, which Directus’s SQL foundation makes straightforward.
Assets and Transformations
Directus stores files in configurable storage, local disk, S3-compatible buckets or cloud storage, and serves them through /assets/{id} with on-the-fly transformations: width, height, fit, quality and format, including WebP and AVIF. Define transformation presets for common sizes and restrict arbitrary transformations in production, so clients cannot request unlimited variants. Put a CDN in front of the assets endpoint with long cache lifetimes; file ids change when a file is replaced, or use the file’s modified_on in the URL as a version. File metadata includes width and height, which the frontend should request with every image to reserve space.
Access Policies and Tokens
Directus permissions are defined per role or policy, per collection and per action, with item-level filters. For a headless frontend, create a read-only policy that allows read on routable collections with a filter such as status equals published, and restrict readable fields to those the site renders, so internal fields never leak through the API. Attach it to a dedicated user with a static token used only on the server, or to the public role if the content is truly public and the API is not otherwise protected. Preview gets its own policy that can read drafts and content versions, with a separate token. The access policies guide walks through the configuration.
Flows for Revalidation
Directus flows run automations on events. A flow triggered by items.create, items.update and items.delete on routable collections can send a request to the frontend’s revalidation endpoint with the collection and keys, signed with a shared secret in a header. Because updates to drafts also fire events, include the item’s status in the request and let the frontend ignore draft changes for public caches. The flows guide shows a complete flow and handler.
Schema Snapshots Across Environments
Directus can export the data model as a snapshot file and apply it to another instance, which makes schema changes reviewable and repeatable. Keep the snapshot in the repository, generate diffs in pull requests, and apply them in CI from development to staging to production. Because collections are SQL tables, destructive changes drop columns and data; apply them in expand-and-contract steps, as described in schema snapshots and migrations.
Security for Self-Hosted Instances
Running Directus yourself makes its security your responsibility. Keep the admin studio and the API on separate hostnames where possible, and restrict the studio to editors through single sign-on and network rules, while the API’s public surface only serves what the read policy allows. Disable public registration, enforce strong authentication for editors, and rotate static tokens used by the frontend. Restrict file uploads by type and size, and serve user-uploaded files from a separate domain to avoid script injection through uploaded HTML or SVG. Apply upgrades promptly, since security fixes land in regular releases, and back up the database and file storage on a schedule you have actually tested restoring. Rate-limit the API at the edge, both to protect the database and to slow down enumeration of public content.
Preview & Draft Workflow
Directus supports several draft models. The simplest is a status field with draft and published values, where the public policy filters on published and the preview policy can read everything. Content versioning adds proper drafts of already published items: editors create a version, change it, preview it and promote it, while the live item stays unchanged. Configure the collection’s preview URL to point at the frontend’s draft route with the item key and, for versions, the version key, and let the frontend fetch with the preview token and bypass caches. Live preview in the studio shows the page side by side with the form and refreshes as editors save.
Typed Fetching with the SDK
The Directus SDK provides a typed client when given a schema type describing collections and their fields. Generate that type from the instance’s schema, or maintain it next to the schema snapshot, and every readItems call becomes type-checked, including selected fields. Keep the SDK on the server, with the read token, inside the data layer that returns domain objects to components.
// lib/directus.ts
import { createDirectus, rest, staticToken, readItems } from "@directus/sdk";
import type { Schema } from "@/types/directus-schema"; // generated from the instance
const client = createDirectus<Schema>(process.env.DIRECTUS_URL!).with(staticToken(process.env.DIRECTUS_READ_TOKEN!)).with(rest({
onRequest: (options) => ({ ...options, next: { revalidate: 3600, tags: ["directus"] } } as RequestInit),
}));
export async function getArticle(slug: string) {
const [article] = await client.request(readItems("articles", {
fields: ["id", "title", "slug", "date_published", { author: ["name"] }],
filter: { slug: { _eq: slug }, status: { _eq: "published" } },
limit: 1,
}));
return article ?? null;
}
The onRequest hook passes framework cache options through the SDK; per-item tags can be added the same way for precise revalidation from flows.
Error Handling & Resilience
Self-hosting means the CMS’s availability is yours to manage. Put the Directus API behind a CDN or the frontend’s data cache so reader traffic rarely reaches it, and serve stale content when it is down. Distinguish permission errors, which usually indicate a policy change, from 5xx errors and timeouts, which indicate load or infrastructure problems, and alert on both. Protect the database with connection limits and query timeouts; an unbounded query with deep relations can saturate it.
Testing & Observability
Test queries against a staging instance seeded from fixtures and the same schema snapshot as production. Monitor API latency by collection and query shape, database load, cache hit rates and flow execution failures, which Directus logs per run. Generate TypeScript types from the schema, with community generators or the OpenAPI specification Directus exposes, and fail CI when they change unexpectedly.
Choosing Directus
Directus fits teams that want control over their data and infrastructure: content lives in their own SQL database, readable by other systems and reporting tools, and the instance runs where they choose, which helps with data residency. It also works as a content layer over an existing database, adding an API and editing studio to tables that already exist. The trade-offs are operational: hosting, upgrades, backups, scaling and security are the team’s responsibility, and some features that SaaS platforms provide out of the box, such as global CDNs for the API, must be assembled. Compared with Strapi, Directus is database-first rather than code-first; compared with SaaS platforms, it trades convenience for control.
Worked Example
A research institute used Directus on top of an existing PostgreSQL database of publications and people, adding an editorial layer for news and landing pages. The first frontend queried Directus directly from the browser with the public role, which exposed internal fields and loaded the API heavily during announcements. The rebuilt integration used a server-side read token with a field-restricted policy, projected queries per template, CDN caching and a flow that revalidated pages on publish. API load at peak dropped by two orders of magnitude, internal fields disappeared from public responses, and the institute’s reporting tools kept reading the same database directly.
Ownership and Operations
Because Directus is self-hosted and database-backed, ownership spans more roles than with a SaaS CMS. The platform or DevOps team owns the instance: hosting, scaling, upgrades, backups and monitoring. The frontend team owns the data layer, policies used by the site, flows that call the frontend, and the schema snapshot workflow. Database administrators, where they exist, own indexes and performance, since slow Directus queries are usually slow SQL. Editors own content and use the studio. Agree on who may change the schema in production, which should be nobody by hand, and on a maintenance window for upgrades.
Frequently Asked Questions
Is Directus a CMS or a database API?
Both. It wraps any supported SQL database with an API and an editing studio, which makes it suitable both as a headless CMS and as a data layer over existing databases.
Should the frontend use REST or GraphQL?
Either. REST with fields projection is simple and cache-friendly; GraphQL suits nested, component-driven queries. Both respect the same permissions.
Can Directus handle localization?
Yes, through translations collections linked to items, with a languages collection. The frontend queries the translation for the requested language and applies fallbacks in code.
How do we scale self-hosted Directus?
Run several stateless instances behind a load balancer with shared database, cache and file storage, and keep most reads at the CDN.
Can Directus run on serverless platforms?
The API needs a persistent Node.js process and a database, so it usually runs on containers or virtual machines. The frontend can be serverless and cache Directus responses at the edge, so reader traffic rarely reaches the instance at all.
How do we upgrade Directus safely?
Upgrade a staging instance first with a copy of production data, run the frontend’s integration tests against it, then upgrade production during a quiet period with a database backup taken immediately before.
Does Directus support real-time updates?
Yes, through WebSocket subscriptions, which are useful for live preview and dashboards. Public pages should still rely on cached responses and revalidation.
Where should Directus run relative to the frontend?
Close to the database and in the same region as the frontend’s servers, so uncached requests stay fast; readers everywhere are served from the CDN regardless of where the instance itself runs.
Can editors change the data model in production?
They should not. Model changes go through schema snapshots in CI, so every environment stays consistent and changes are reviewed.
Related
- Directus as a Headless Data Layer for Jamstack
- Directus Custom API Extensions for Frontend Apps
- Directus Flows for Webhook-Driven Revalidation
- Directus Access Policies for Public and Preview Tokens
- Directus Content Versioning for Draft Previews
- Directus Schema Snapshots and Migrations Across Environments