Next.js
Procedure API for Next.js App Router
Next.js
The Next.js adapter provides full integration with the App Router.
import { init } from "@jlnstack/procedure/next"init
Initialize Procedure with a base context:
const { procedure, middleware } = init({
ctx: async () => ({ db: getDb() })
})procedure
Creates a new procedure builder with Next.js specific methods:
const handler = procedure
.params<{ slug: string }>()
.rsc(...).use()
Adds a middleware to the procedure:
procedure.use(withAuth)
procedure.use([middlewareA, middlewareB]) // parallel.params()
Declares route parameters. The type should match your route structure:
// app/posts/[slug]/page.tsx
const getPost = procedure
.params<{ slug: string }>()
.run(async ({ ctx }) => {
return ctx.db.posts.findBySlug(ctx.params.slug)
}).searchParams()
Validates and types search parameters using any Standard Schema compatible validator (Zod, Valibot, ArkType, etc.):
import { z } from "zod"
const searchSchema = z.object({
page: z.coerce.number().default(1),
sort: z.enum(["asc", "desc"]).default("desc")
})
const getPosts = procedure
.searchParams(searchSchema)
.run(async ({ ctx }) => {
return ctx.db.posts.findMany({
page: ctx.searchParams.page,
sort: ctx.searchParams.sort
})
}).rsc()
Finalizes as a React Server Component:
const UserAvatar = procedure
.use(withAuth)
.rsc(async ({ ctx }) => {
return <Avatar src={ctx.user.avatar} name={ctx.user.name} />
})Use it in any server component:
export default function Header() {
return (
<header>
<Logo />
<UserAvatar />
</header>
)
}.page()
Finalizes as a Next.js page component. Automatically receives searchParams:
// rsc.ts
export const PostPage = procedure
.params<{ slug: string }>()
.searchParams(schema)
.page(async ({ ctx }) => {
const post = await ctx.db.posts.find(ctx.params.slug)
return <Article post={post} page={ctx.searchParams.page} />
})
// page.tsx
import { PostPage } from "./rsc"
export default PostPage.layout()
Finalizes as a Next.js layout component. Receives children alongside the context:
// rsc.ts
export const PostLayout = procedure
.params<{ slug: string }>()
.layout(async ({ children, ctx }) => {
const post = await ctx.db.posts.find(ctx.params.slug)
return (
<div>
<Sidebar post={post} />
{children}
</div>
)
})
// layout.tsx
import { PostLayout } from "./rsc"
export default PostLayout.metadata()
Generates Next.js metadata:
// rsc.ts
export const generateMetadata = procedure
.params<{ slug: string }>()
.metadata(async ({ ctx }) => {
const post = await ctx.db.posts.find(ctx.params.slug)
return {
title: post.title,
description: post.excerpt
}
})
// page.tsx
export { generateMetadata } from "./rsc".layoutMetadata()
Generates metadata for layouts (only receives params, not searchParams):
export const generateMetadata = procedure
.params<{ slug: string }>()
.layoutMetadata(async ({ ctx }) => ({
title: `Posts - ${ctx.params.slug}`
})).staticParams()
Generates static params for generateStaticParams:
// rsc.ts
export const generateStaticParams = procedure
.params<{ slug: string }>()
.staticParams(async ({ ctx }) => {
const posts = await ctx.db.posts.findMany()
return posts.map((p) => ({ slug: p.slug }))
})
// page.tsx
export { generateStaticParams } from "./rsc"Reusable Procedures
Create reusable procedure builders with shared middlewares:
// lib/i18n-procedure.ts
const withI18n = middleware(async ({ ctx, next }) => {
const t = await initTranslations(ctx.params.locale)
return next({ ctx: { ...ctx, t } })
})
export const i18nProcedure = procedure
.params<{ locale: string }>()
.use(withI18n)Use the same procedure for both page and metadata:
// app/[locale]/about/rsc.ts
import { i18nProcedure } from "@/lib/i18n-procedure"
export const AboutPage = i18nProcedure.page(async ({ ctx }) => {
return (
<main>
<h1>{ctx.t("about.title")}</h1>
<p>{ctx.t("about.description")}</p>
</main>
)
})
export const generateMetadata = i18nProcedure.metadata(async ({ ctx }) => ({
title: ctx.t("about.title"),
description: ctx.t("about.description")
}))// app/[locale]/about/page.tsx
import { AboutPage, generateMetadata } from "./rsc"
export default AboutPage
export { generateMetadata }