Next.js Integration
Complete guide to using Dyrected in a Next.js App Router project.
Dyrected integrates natively with the Next.js App Router via a catch-all API route. You can fetch data server-side, client-side, or use the SDK in Server Components.
Installation
pnpm add @dyrected/core @dyrected/sdk @dyrected/next @dyrected/db-postgres[!NOTE] The
@dyrected/nextpackage provides the specialized<DyrectedAdmin />component and utilities for seamless Next.js App Router integration.
Step 1 — Create your config
// dyrected.config.ts (project root)
import { defineConfig } from '@dyrected/core'
import { PostgresAdapter } from '@dyrected/db-postgres'
export default defineConfig({
db: new PostgresAdapter({ url: process.env.DATABASE_URL! }),
collections: [
{
slug: 'posts',
admin: { useAsTitle: 'title' },
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', required: true, unique: true },
{ name: 'body', type: 'richText' },
{
name: 'status',
type: 'select',
options: ['draft', 'published'],
defaultValue: 'draft',
},
],
},
],
})Step 2 — Mount the API route
Create a catch-all route that forwards all Dyrected requests to the Hono router:
// app/dyrected/[...route]/route.ts
import { createDyrectedApp } from '@dyrected/core/server'
import config from '@/dyrected.config'
const app = createDyrectedApp(config)
export const GET = app.fetch
export const POST = app.fetch
export const PATCH = app.fetch
export const DELETE = app.fetchAll Dyrected REST endpoints are now available at /dyrected/....
Step 3 — Fetch data in Server Components
// app/blog/page.tsx
import { createClient } from '@dyrected/sdk'
const client = createClient({
baseUrl: process.env.NEXT_PUBLIC_DYRECTED_URL!,
apiKey: process.env.DYRECTED_API_KEY!,
})
export default async function BlogPage() {
const { docs: posts } = await client.collection('posts').find({
where: { status: { equals: 'published' } },
sort: '-createdAt',
depth: 1,
})
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}When Dyrected is co-located with your Next.js app (self-hosted), you can bypass HTTP entirely:
// app/blog/page.tsx
import config from '@/dyrected.config'
export default async function BlogPage() {
const { docs: posts } = await config.db.find({
collection: 'posts',
where: { status: { equals: 'published' } },
limit: 10,
})
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}For environments where the SDK isn't available, use raw HTTP:
// app/blog/page.tsx
export default async function BlogPage() {
const res = await fetch(
`${process.env.NEXT_PUBLIC_DYRECTED_URL}/collections/posts?where[status][equals]=published`,
{
headers: { 'x-api-key': process.env.DYRECTED_API_KEY! },
next: { revalidate: 3600 }
}
)
const { docs: posts } = await res.json()
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}This approach has zero network overhead and is the fastest option for self-hosted setups.
Step 4 — Embed the Admin UI
Create a catch-all page at app/admin/[[...route]]/page.tsx. This ensures that the root /admin and all internal admin sub-routes are handled by this single page.
The @dyrected/next package provides a <DyrectedAdmin /> component that handles all the Next.js router integration, CSS imports, and "Client Only" mounting for you.
// app/admin/[[...route]]/page.tsx
import { DyrectedAdmin } from '@dyrected/next/admin'
export default function AdminPage() {
return (
<DyrectedAdmin
basename="/admin"
/>
)
}[!TIP]
DyrectedAdminautomatically reads your environment variables (likeNEXT_PUBLIC_DYRECTED_URL) if they follow the standard naming convention. You only need to pass props if you want to override the defaults.
Environment Variables
# .env.local
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
# Dyrected API base URL (the Next.js app URL + /dyrected)
NEXT_PUBLIC_DYRECTED_URL=http://localhost:3000/dyrected
# Server-side API key (never expose to browser)
DYRECTED_API_KEY=sk_live_...
# Client-side keys (safe to expose — access is controlled by Dyrected access functions)
NEXT_PUBLIC_DYRECTED_API_KEY=pk_live_...
NEXT_PUBLIC_SITE_ID=site_...ISR Cache Revalidation
When content is published, you often want to revalidate cached Next.js pages. Use an afterChange hook to call revalidatePath or revalidateTag:
// dyrected.config.ts
import { revalidatePath } from 'next/cache'
{
slug: 'posts',
hooks: {
afterChange: [
async ({ doc, operation }) => {
if (doc.status === 'published') {
revalidatePath('/blog')
revalidatePath(`/blog/${doc.slug}`)
}
}
]
}
}Or use webhook-style revalidation via a dedicated endpoint:
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'
export async function POST(req: NextRequest) {
const secret = req.headers.get('x-revalidate-secret')
if (secret !== process.env.REVALIDATE_SECRET) {
return new Response('Unauthorized', { status: 401 })
}
const { path } = await req.json()
revalidatePath(path)
return Response.json({ revalidated: true })
}Live Preview
Add live preview support to any page with the useLivePreview hook:
// app/blog/[slug]/preview/page.tsx
'use client'
import { useLivePreview } from '@dyrected/react'
export default function BlogPreview({ initialPost }: { initialPost: Post }) {
const { data: post, isLive } = useLivePreview({
initialData: initialPost,
serverURL: process.env.NEXT_PUBLIC_DYRECTED_ADMIN_URL!,
})
return (
<article>
{isLive && <div className="preview-badge">Preview Mode</div>}
<h1>{post.title}</h1>
</article>
)
}Enable it on the collection:
{
slug: 'posts',
admin: {
previewUrl: (doc) =>
doc?.slug ? `${process.env.NEXT_PUBLIC_SITE_URL}/blog/${doc.slug}/preview` : null,
}
}See Live Preview for the full guide.
Middleware & Route Protection
Protect your admin route with Next.js middleware:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/admin')) {
const token = request.cookies.get('dyrected-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/admin/login', request.url))
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/admin/:path*'],
}