# Create Custom Middleware URL: /docs/guides/custom-middleware Create Custom Middleware [#create-custom-middleware] Build middleware with `defineMiddleware` to intercept and transform kernel operations. Middleware uses an onion-model wrap pattern: code after `handler()` runs on the return journey. Prerequisites [#prerequisites] * [Install @taucad/runtime](../getting-started/installation) * Completed the [Quick Start](../getting-started/quick-start) * [Use Middleware](./using-middleware) -- Add built-in middleware to your client * [Middleware Model](../concepts/middleware-model) -- Understand the onion model Goal [#goal] Create a logging middleware and a stateful caching middleware. Learn the wrap pattern, `stateSchema`, and `optionsSchema`. Steps [#steps] 1. Import defineMiddleware [#1-import-definemiddleware] Import from `@taucad/runtime/middleware`: ```typescript import { defineMiddleware } from '@taucad/runtime/middleware'; ``` 2. Create a Simple Logging Middleware [#2-create-a-simple-logging-middleware] Use `wrapCreateGeometry` (or `wrapGetParameters`, `wrapExportGeometry`) to log before and after the operation. Call `handler(input)` to continue the chain: ```typescript import { defineMiddleware } from '@taucad/runtime/middleware'; const loggingMiddleware = defineMiddleware({ name: 'Logging', async wrapCreateGeometry(input, handler, { logger }) { logger.debug('Computing geometry...'); const result = await handler(input); logger.debug('Geometry computed'); return result; }, }); ``` Code before `handler()` runs on the way down; code after runs on the way back up (onion model). 3. Use the Onion Model Wrap Pattern [#3-use-the-onion-model-wrap-pattern] The wrap pattern gives you full control. You can short-circuit (return without calling `handler`), transform input, or transform output: ```typescript @ts-nocheck import { defineMiddleware } from '@taucad/runtime/middleware'; // Application-specific cache — replace with your storage backend const cache = new Map(); const checkCache = async (key: string) => cache.get(key) ?? null; const writeCache = async (key: string, value: unknown) => { cache.set(key, value); }; const cacheMiddleware = defineMiddleware({ name: 'SimpleCache', async wrapCreateGeometry(input, handler, { logger, dependencyHash }) { const cached = await checkCache(dependencyHash); if (cached) { logger.debug('Cache hit'); return cached; } const result = await handler(input); await writeCache(dependencyHash, result); return result; }, }); ``` Short-circuited results still flow through upstream middleware because each layer wraps the next. 4. Add Stateful Middleware with stateSchema [#4-add-stateful-middleware-with-stateschema] Use `stateSchema` (Zod) for type-safe state that persists during the operation. Access `state` from the runtime: ```typescript @ts-nocheck import { defineMiddleware } from '@taucad/runtime/middleware'; import { z } from 'zod'; const statefulCacheMiddleware = defineMiddleware({ name: 'StatefulCache', stateSchema: z.object({ cacheKey: z.string().optional(), cacheHit: z.boolean().optional(), }), async wrapCreateGeometry(input, handler, { logger, state, dependencyHash }) { const cacheKey = dependencyHash; const cached = await checkCache(cacheKey); if (cached) { state.update({ cacheKey, cacheHit: true }); return cached; } state.update({ cacheKey, cacheHit: false }); const result = await handler(input); await writeCache(cacheKey, result); return result; }, }); ``` `state.update()` merges partial data into the state. Values are validated against `stateSchema`. 5. Add optionsSchema for Configuration [#5-add-optionsschema-for-configuration] Use `optionsSchema` for configurable middleware. The schema supports `.default()` for optional options: ```typescript @ts-nocheck import { defineMiddleware } from '@taucad/runtime/middleware'; import { z } from 'zod'; const configurableCacheMiddleware = defineMiddleware({ name: 'ConfigurableCache', optionsSchema: z.object({ maxAgeMs: z.number().default(60_000), enabled: z.boolean().default(true), }), stateSchema: z.object({ cacheKey: z.string().optional(), }), async wrapCreateGeometry(input, handler, { options, state, dependencyHash }) { if (!options.enabled) { return handler(input); } const cached = await checkCache(dependencyHash, options.maxAgeMs); if (cached) { state.update({ cacheKey: dependencyHash }); return cached; } const result = await handler(input); await writeCache(dependencyHash, result, options.maxAgeMs); return result; }, }); ``` Options are resolved from schema defaults and caller overrides when the middleware is registered. 6. Export and Register Your Middleware [#6-export-and-register-your-middleware] Middleware runs in the worker and must be loaded as a separate module. Create a module file that exports the middleware (default or named with a `name` property), then a factory that returns a [`MiddlewarePlugin`](../api/middleware): ```typescript @ts-nocheck // my-cache.middleware.ts import { defineMiddleware } from '@taucad/runtime/middleware'; import { z } from 'zod'; const myCacheMiddleware = defineMiddleware({ name: 'MyCache', stateSchema: z.object({ cacheKey: z.string().optional() }), async wrapCreateGeometry(input, handler, { state, dependencyHash }) { const cached = await checkCache(dependencyHash); if (cached) return cached; const result = await handler(input); await writeCache(dependencyHash, result); return result; }, }); export default myCacheMiddleware; ``` ```typescript @ts-nocheck // my-cache-plugin.ts import type { MiddlewarePlugin } from '@taucad/runtime'; export function myCache(): MiddlewarePlugin { return { id: 'my-cache', moduleUrl: new URL('./my-cache.middleware.js', import.meta.url).href, options: {}, }; } ``` Pass the factory to [`createRuntimeClient`](../api/client): ```typescript @ts-nocheck import { createRuntimeClient } from '@taucad/runtime'; import { fromNodeFS } from '@taucad/runtime/filesystem/node'; import { replicad } from '@taucad/runtime/kernels'; import { esbuild } from '@taucad/runtime/bundler'; import { myCache } from './my-cache-plugin.js'; const client = createRuntimeClient({ kernels: [replicad()], bundlers: [esbuild()], fileSystem: fromNodeFS('/path/to/project'), middleware: [myCache()], }); ``` 7. Wrap Multiple Operations [#7-wrap-multiple-operations] Implement `wrapGetParameters` and `wrapExportGeometry` when needed: ```typescript @ts-nocheck const fullMiddleware = defineMiddleware({ name: 'FullMiddleware', async wrapGetParameters(input, handler, { logger }) { logger.debug('Getting parameters'); return handler(input); }, async wrapCreateGeometry(input, handler, { logger }) { logger.debug('Creating geometry'); return handler(input); }, async wrapExportGeometry(input, handler, { logger }) { logger.debug('Exporting geometry'); return handler(input); }, }); ``` Variations [#variations] * **Version**: Set `version` for cache-key computation. Changing the version invalidates caches that include middleware signatures. * **Runtime services**: Destructure `logger`, `filesystem`, `dependencies`, `dependencyHash`, `state`, and `options` from the `KernelMiddlewareRuntime` as needed. * **Inline middleware**: For testing or single-app use, you can pass middleware objects directly if your setup supports it. Production use typically requires a module URL for worker loading. Related [#related] * [Middleware Model](../concepts/middleware-model) -- Onion model and execution order * [Use Middleware](./using-middleware) -- Add built-in middleware * [API Reference: Middleware](../api/middleware) -- defineMiddleware and types