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 schelo

You need Zod as a peer dependency. If it is not already installed:

pnpm add schelo zod

1. Run the CLI

From your Next.js app folder:

npx schelo init

Follow 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

WhatWhere (typical paths)
Interceptor configlib/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

  1. Import the provider using the path the CLI printed (default export from InterceptorProvider.tsx, or a named re-export from your providers index if present).
  2. 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>
  );
}

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;

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>
  );
}

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]),
    },
  },
});

Swap the paths and schemas for your real API. See Configuration & API for mode, validate: false, and other options.