Exemples full-stack
Les exemples full-stack utilisent des scopes pour les loaders, actions, gestionnaires de routes et Server Actions. En développement, la racine est conservée sur globalThis pour éviter de dupliquer les clients pendant le HMR.
Les deux exemples utilisent le même graphe. Compare la limite de l’opération dont chaque framework attend la fin. examples/_shared/container.ts
| Exemple | Contenu |
|---|---|
next-app-router.ts | Scopes de requête et de Server Action avec Next.js App Router |
remix.ts | Scopes de loader et d’action Remix |
Next.js App Router
ts
'use server'
import { revalidatePath } from 'next/cache'
import { buildRootContainer, type RootContainer } from '../_shared/container.js'
/*
* In development, HMR can reload this module without resetting globalThis.
* Reusing the root avoids leaking DB/cache clients on every file save and
* guarantees that across reloads `buildRootContainer()` runs ONCE, so the
* async `Database` factory in `_shared/container.ts` is initialized once
*/
const globalForInferDI = globalThis as typeof globalThis & {
__inferdiNextRoot?: RootContainer
}
export const root: RootContainer =
process.env.NODE_ENV === 'production'
? buildRootContainer()
: (globalForInferDI.__inferdiNextRoot ??= buildRootContainer())
/*
* Server Actions run as a single bounded async unit and Next awaits the
* function before returning to the client. `await using` is the right tool
* here: the scope is asyncDispose'd exactly when the action finishes,
* including on thrown errors
*/
export async function getProfileAction(formData: FormData) {
const userId = String(formData.get('userId') ?? '') || undefined
await using scope = root.createScope({
request: {
requestId: crypto.randomUUID(),
userId
}
})
const users = await scope.getAsync('users')
const profile = await users.profile(userId ?? 'me')
revalidatePath('/profile')
return profile
}Fichier du dépôt : examples/fullstack/next-app-router.ts
Remix
ts
import type {
ActionFunctionArgs,
LoaderFunctionArgs
} from '@remix-run/node'
import { json } from '@remix-run/node'
import {
buildRootContainer,
createRequestScope,
type RootContainer
} from '../_shared/container.js'
/*
* Remix dev can re-evaluate modules during HMR while globalThis survives.
* Cache the root so the async Database factory is not re-run on every save
*/
const globalForInferDI = globalThis as typeof globalThis & {
__inferdiRemixRoot?: RootContainer
}
export const root: RootContainer =
process.env.NODE_ENV === 'production'
? buildRootContainer()
: (globalForInferDI.__inferdiRemixRoot ??= buildRootContainer())
async function scopeFor(request: Request) {
return createRequestScope(root, {
requestId: request.headers.get('x-request-id') ?? crypto.randomUUID(),
userId: request.headers.get('x-user-id') ?? undefined
})
}
export async function loader({ request }: LoaderFunctionArgs) {
/*
* Loader / action functions are bounded async operations that Remix awaits
* before serializing the response. `await using` ties scope disposal to
* the same boundary — Remix never streams data out of a loader after it
* returns, so the scope is safe to tear down here
*/
await using scope = await scopeFor(request)
const users = await scope.getAsync('users')
return json(await users.profile('me'))
}
export async function action({ request }: ActionFunctionArgs) {
await using scope = await scopeFor(request)
const form = await request.formData()
return json({ ok: true, fields: [...form.keys()] })
}Fichier du dépôt : examples/fullstack/remix.ts
