Skip to main content

sdk-patterns

@directus/sdk patterns — composable client, TypeScript types, CRUD operations, authentication, real-time subscriptions, server-side use in Next.js (fetch options, per-request tokens, relations in queries). This skill should be used when the user asks about "Directus SDK", "@directus/sdk", "Directus client library", "Directus TypeScript", "Directus in Next.js", or needs code patterns for integrating Directus into a JavaScript/TypeScript project.

Source facts

Repository
Agents-Store/codex-plugins
Last source activity
November 1, 2026 at 02:46
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
7 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
sdk-patterns
description
@directus/sdk patterns — composable client, TypeScript types, CRUD operations, authentication, real-time subscriptions, server-side use in Next.js (fetch options, per-request tokens, relations in queries). This skill should be used when the user asks about "Directus SDK", "@directus/sdk", "Directus client library", "Directus TypeScript", "Directus in Next.js", or needs code patterns for integrating Directus into a JavaScript/TypeScript project.
# @directus/sdk Patterns The official Directus SDK uses a composable architecture. Install: ```bash npm install @directus/sdk ``` Requirements and versioning (verified against `@directus/sdk` 26.0.0, built for Directus 12.4.x): - Node.js 22 or later (`engines.node >= 22`). The SDK needs global `fetch` and `URL`; `WebSocket` only for `realtime()`. - Use the SDK release that belongs to your server release: 26.x for Directus 12.4.x. A typed client from another generation can send calls the server no longer has. - Every snippet below reads the server address and token from the environment: `DIRECTUS_URL` and `DIRECTUS_TOKEN` (server-side), never hardcode them. ## Client Setup ### Basic Client (Static Token, server-side) ```typescript import { createDirectus, rest, staticToken } from '@directus/sdk'; const client = createDirectus(process.env.DIRECTUS_URL!) .with(staticToken(process.env.DIRECTUS_TOKEN!)) .with(rest()); ``` A static token belongs to one Directus user and carries that user's permissions. Keep it on the server, in an environment variable (`${DIRECTUS_TOKEN}` in `.env`), and give the user a policy with only the access the code needs. ### Client with Login (Node.js scripts and servers) ```typescript import { createDirectus, rest, authentication } from '@directus/sdk'; // 'json' mode: the refresh token comes back in the response body and the client keeps it, // so auto-refresh works outside a browser. The default mode is 'cookie': in Node it returns no refresh token, so the session cannot be refreshed. const client = createDirectus(process.env.DIRECTUS_URL!) .with(authentication('json')) .with(rest()); // Login takes ONE payload object, then optional options await client.login({ email: process.env.DIRECTUS_EMAIL!, password: process.env.DIRECTUS_PASSWORD!, }); // Token is managed automatically (refresh handled internally) // One-shot scripts: the client schedules the next token refresh with a timer, // which keeps Node alive. Stop it when the script is done (or build the client // with authentication('json', { autoRefresh: false })). client.stopRefreshing(); ``` `login(payload, options)`. The payload is `{ email, password }`, or `{ identifier, password }` for an LDAP provider. Options (all optional): ```typescript await client.login( { email, password }, { otp: '123456', // one-time password when the user has 2FA mode: 'json', // 'json' | 'cookie' | 'session'; overrides the mode given to authentication() provider: 'default', // pick an auth provider (not for redirect-based SSO) }, ); await client.login({ identifier: 'username', password }, { provider: 'ldap' }); ``` The positional form `login(email, password)` was replaced in SDK 20. It is a type error in TypeScript and throws at runtime in JavaScript. `refresh` and `logout` take an options object too (`{ mode, refresh_token }`), not positional arguments: ```typescript await client.refresh(); // uses the stored session await client.logout(); // as a standalone REST command: client.request(refresh({ mode: 'json', refresh_token })) ``` ### Client with Login (browser app, session cookie) ```typescript import { createDirectus, rest, authentication } from '@directus/sdk'; const client = createDirectus(process.env.NEXT_PUBLIC_DIRECTUS_URL!) .with(authentication('session', { credentials: 'include' })) .with(rest({ credentials: 'include' })); await client.login({ email, password }); ``` Directus and the app on different domains need `credentials: 'include'` on both composables, plus matching CORS (`CORS_ORIGIN`) and cookie settings on the server. For SSO providers that redirect in the browser (Google, GitHub, Okta), `login()` does not apply: send the user to `/auth/login/<provider>?redirect=<app-url>` and call `client.refresh()` after they return. SSO needs a licensed tier on Directus 12. ### Reading and Setting the Token ```typescript const token = await client.getToken(); // current access token or null await client.setToken(accessToken); // use a token you obtained elsewhere ``` ### Reusable Client Module One module per app keeps the typed schema and the auth choice in a single place. Use a static-token client for server code and a token-less client for public reads that the Public policy allows: ```typescript // lib/directus.ts import { createDirectus, rest, staticToken } from '@directus/sdk'; import type { Schema } from './directus-schema'; // your MySchema interface const url = process.env.DIRECTUS_URL!; /** Server-only: acts as the token's user. */ export function serverClient() { return createDirectus<Schema>(url) .with(staticToken(process.env.DIRECTUS_TOKEN!)) .with(rest()); } /** Anonymous: only what the Public policy grants. */ export const publicClient = createDirectus<Schema>(url).with(rest()); ``` Build a fresh client inside background jobs and serverless handlers instead of importing a shared singleton, so every invocation reads its own environment. ## Type-Safe Schema Define your schema for TypeScript autocompletion: ```typescript interface Post { id: string; title: string; content: string; status: 'draft' | 'published' | 'archived'; author: string | Author; categories: string[] | PostCategory[]; date_created: string; } interface Author { id: string; first_name: string; last_name: string; email: string; } interface Category { id: string; name: string; slug: string; } interface PostCategory { id: string; posts_id: string | Post; categories_id: string | Category; } interface MySchema { posts: Post[]; authors: Author[]; categories: Category[]; posts_categories: PostCategory[]; } const client = createDirectus<MySchema>(process.env.DIRECTUS_URL!) .with(staticToken(process.env.DIRECTUS_TOKEN!)) .with(rest()); ``` Match the schema to the real collection. The string `status` above is the classic draft/published/archived pattern; collections created in Directus 12 get a boolean `archived` field instead (`archived: boolean`), and existing `status` fields keep working. For publish workflows prefer content versions (below) over a status flag. System collections are typed by the SDK itself (`DirectusUser`, `DirectusRole`, `DirectusPolicy` and so on). Since SDK 26 several of their fields are nullable (`DirectusRole.parent`, `DirectusVersion.hash`, `DirectusRelation.meta` and `.schema`) and `DirectusPolicy.ip_access` is `string[] | null`: add null checks when you read them. ## CRUD Operations ### Read Items ```typescript import { readItems } from '@directus/sdk'; // List with filter and sort const posts = await client.request( readItems('posts', { fields: ['id', 'title', 'status', { author: ['first_name', 'last_name'] }], filter: { status: { _eq: 'published' } }, sort: ['-date_created'], limit: 25, }) ); ``` ### Read Single Item ```typescript import { readItem } from '@directus/sdk'; const post = await client.request( readItem('posts', 'item-uuid', { fields: ['*', { author: ['*'] }], }) ); ``` ### Create Item ```typescript import { createItem } from '@directus/sdk'; const newPost = await client.request( createItem('posts', { title: 'New Post', content: 'Post content...', status: 'draft', author: 'author-uuid', }) ); ``` ### Create Multiple Items ```typescript import { createItems } from '@directus/sdk'; const newPosts = await client.request( createItems('posts', [ { title: 'Post 1', status: 'draft' }, { title: 'Post 2', status: 'draft' }, ]) ); ``` ### Update Item ```typescript import { updateItem } from '@directus/sdk'; const updated = await client.request( updateItem('posts', 'item-uuid', { status: 'published', }) ); ``` ### Update Multiple Items ```typescript import { updateItems } from '@directus/sdk'; const updated = await client.request( updateItems('posts', ['uuid-1', 'uuid-2'], { status: 'archived', }) ); ``` ### Delete Item ```typescript import { deleteItem, deleteItems } from '@directus/sdk'; // Single await client.request(deleteItem('posts', 'item-uuid')); // Multiple await client.request(deleteItems('posts', ['uuid-1', 'uuid-2'])); ``` ## Filtering ```typescript const results = await client.request( readItems('products', { filter: { _and: [ { status: { _eq: 'active' } }, { _or: [ { price: { _lt: 50 } }, { featured: { _eq: true } }, ], }, ], }, }) ); ``` ## Aggregation ```typescript import { aggregate } from '@directus/sdk'; const stats = await client.request( aggregate('orders', { aggregate: { count: '*', sum: 'total', avg: 'total' }, groupBy: ['status'], }) ); ``` ## Search ```typescript const results = await client.request( readItems('articles', { search: 'machine learning', fields: ['id', 'title', 'excerpt'], limit: 20, }) ); ``` ## Deep Queries ```typescript const posts = await client.request( readItems('posts', { fields: ['title', { comments: ['text', { author: ['name'] }] }], deep: { comments: { _filter: { status: { _eq: 'approved' } }, _sort: ['-date_created'], _limit: 5, }, }, }) ); ``` ## File Operations ```typescript import { uploadFiles, importFile } from '@directus/sdk'; // Upload from form data const formData = new FormData(); formData.append('file', fileBlob); formData.append('title', 'My Image'); const uploaded = await client.request(uploadFiles(formData)); // Import from URL const imported = await client.request( importFile('https://example.com/image.jpg', { title: 'Imported Image', folder: 'folder-uuid', }) ); ``` ## Schema Operations ```typescript import { readCollections, createCollection, readFields, createField, readRelations, createRelation, schemaSnapshot, schemaDiff, schemaApply, } from '@directus/sdk'; // Read all collections const collections = await client.request(readCollections()); // Get schema snapshot for migration (admin token required) const snapshot = await client.request(schemaSnapshot()); // Partial snapshot: pass exactly one of includeCollections / excludeCollections const partial = await client.request(schemaSnapshot({ includeCollections: ['posts'] })); // Compare against a snapshot from another environment. // SDK 26: the second argument is an options object, not a boolean. const diff = await client.request( schemaDiff(snapshotFromStaging, { force: false, mode: 'mirror' }), // 'merge' = additive, no deletions ); // Apply the diff if (diff && Object.keys(diff.diff).length > 0) { await client.request(schemaApply(diff)); } ``` `schemaDiff(snapshot, true)` (positional `force`) was replaced by `schemaDiff(snapshot, { force: true })` in Directus 12.2.0 so that `mode` could be added. Snapshots sent to the server are limited by `IMPORT_MAX_FILE_SIZE` (default `50mb`). SDK 26 validates some parameters before sending: `readRelationByCollection`, `createField`, `deleteCollection`, `utilsExport`, `utilsImport`, `utilitySort`, `triggerFlow` and `readShareInfo` throw immediately when given an empty collection, key or id. Check values that come from user input or config before you call them. ## Server-Side Apps (Next.js and Other SSR Frameworks) Calling Directus from Server Components, Route Handlers or Server Actions has its own rules. They live in two files, loaded on demand:
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub