Next.js setup
Use the CLI to scaffold config and the client provider, then wrap your root layout yourself—the CLI prints the exact import path for your project.
Install
Add the package to your Next.js app:
pnpm add schelopnpm add scheloYou need Zod as a peer dependency. If it is not already installed:
pnpm add schelo zodpnpm add schelo zod1. Run the CLI
From your Next.js app folder:
npx schelo initnpx schelo initFollow the prompts (framework and mode). When it finishes, you will have the files below and printed instructions for your root layout.
2. What gets created
| What | Where (typical paths) |
|---|---|
| Interceptor config | lib/api-schemas.ts or src/lib/api-schemas.ts |
| Client provider (enables fetch validation in the browser) | components/providers/InterceptorProvider.tsx or src/components/providers/InterceptorProvider.tsx |
| Barrel export (only if that file already exists) | components/providers/index.ts or src/components/providers/index.ts |
Layout: the CLI does not edit app/layout.tsx (or src/app/layout.tsx). It prints copy-paste steps so you stay in control.
3. Wire the provider in your root layout
- Import the provider using the path the CLI printed (default export from
InterceptorProvider.tsx, or a named re-export from your providersindexif present). - Wrap
{children}(and your other providers) with it.
Example shape:
import InterceptorProvider from "@/components/providers/InterceptorProvider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<InterceptorProvider>{children}</InterceptorProvider>
</body>
</html>
);
}import InterceptorProvider from "@/components/providers/InterceptorProvider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<InterceptorProvider>{children}</InterceptorProvider>
</body>
</html>
);
}Your import path may differ (for example @/src/components/...); follow the CLI output.
Manual setup
Skip npx schelo init if you want full control. Add a client provider, wire it in your App Router root layout, and define routes in lib/api-schemas.ts (or src/lib/api-schemas.ts).
1. Provider file
Create components/providers/InterceptorProvider.tsx (or under src/components/providers/). It must be a client component so enable() runs in the browser. The import below uses the @/lib/... alias; if yours differs, use a relative path from the provider file (for example ../../lib/api-schemas from components/providers/).
"use client";
import type { ReactNode } from "react";
import { useEffect } from "react";
import { interceptor } from "@/lib/api-schemas";
type Props = {
children: ReactNode;
};
const InterceptorProvider = ({ children }: Props) => {
useEffect(() => {
interceptor.enable();
return () => interceptor.disable();
}, []);
return <>{children}</>;
};
export default InterceptorProvider;"use client";
import type { ReactNode } from "react";
import { useEffect } from "react";
import { interceptor } from "@/lib/api-schemas";
type Props = {
children: ReactNode;
};
const InterceptorProvider = ({ children }: Props) => {
useEffect(() => {
interceptor.enable();
return () => interceptor.disable();
}, []);
return <>{children}</>;
};
export default InterceptorProvider;2. Wrap children in the root layout
In app/layout.tsx (or src/app/layout.tsx), import the provider and wrap {children}. Put other client providers inside or outside as you prefer—keep InterceptorProvider in the tree so all client fetch calls run after mount.
import InterceptorProvider from "@/components/providers/InterceptorProvider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<InterceptorProvider>{children}</InterceptorProvider>
</body>
</html>
);
}import InterceptorProvider from "@/components/providers/InterceptorProvider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<InterceptorProvider>{children}</InterceptorProvider>
</body>
</html>
);
}Adjust the import to match your project (for example @/src/components/providers/InterceptorProvider). If you use a barrel file with export { default as InterceptorProvider }from "./InterceptorProvider", you can use a named import from that index instead.
3. lib/api-schemas.ts
Export an interceptor from createInterceptor. Route keys use METHOD /path (dynamic segments like :id are supported). Add request and/or response Zod schemas per route.
import { createInterceptor } from "schelo";
import { z } from "zod";
const apiErrorSchema = z.object({ error: z.string() });
export const interceptor = createInterceptor({
mode: "warn",
warnOnUnmatched: true,
routes: {
"GET /api/health": {
response: z.union([
z.object({ status: z.literal("ok") }),
apiErrorSchema,
]),
},
"POST /api/items": {
request: z.object({ title: z.string() }),
response: z.union([z.object({ id: z.string() }), apiErrorSchema]),
},
},
});import { createInterceptor } from "schelo";
import { z } from "zod";
const apiErrorSchema = z.object({ error: z.string() });
export const interceptor = createInterceptor({
mode: "warn",
warnOnUnmatched: true,
routes: {
"GET /api/health": {
response: z.union([
z.object({ status: z.literal("ok") }),
apiErrorSchema,
]),
},
"POST /api/items": {
request: z.object({ title: z.string() }),
response: z.union([z.object({ id: z.string() }), apiErrorSchema]),
},
},
});Swap the paths and schemas for your real API. See Configuration & API for mode, validate: false, and other options.