functions-node
Type-safe handler definers, invocation helpers, and TypeScript types for building Sanity Functions on Node.js.
Repository Health
Technical Analysis
@sanity/functions is the official helper library for writing Sanity Functions in Node.js. It exports typed handler definers — documentEventHandler, scheduledEventHandler, syncTagInvalidateEventHandler, and pubSubEventHandler — that wrap a function’s handler with compile-time event and context typing plus a lightweight runtime check that the value passed in is actually a function.
Beyond the definers, the package ships an invoke() helper for calling one Sanity Function from another over SNS, SQS, or Lambda via aws-lite, complete with lineage-token tracking that caps recursive invocation depth, and an experimental createDurable() API for defining durable, resumable functions. Because the platform integration (aws-lite clients, resource discovery via DynamoDB) lives behind peer dependencies, most consumers only ever touch the type-safe surface exposed from the package, running inside Sanity’s own managed Functions runtime.
What You Get
- Typed handler definers for document, scheduled, pubsub, and sync-tag-invalidate events
- An invoke() helper for calling other Sanity Functions synchronously or asynchronously
- Automatic lineage-token tracking that caps recursive function-to-function invocation depth
- An experimental createDurable() API for defining durable, resumable functions
- Full TypeScript types for function context, event payloads, and resources
Common Use Cases
- Reacting to document publish/unpublish/mutation events with a typed handler
- Running scheduled, cron-like maintenance or sync jobs inside a Sanity project
- Invalidating downstream caches when GROQ sync tags change
- Chaining Sanity Functions together via invoke(), with recursion-depth protection
- Returning a computed result from a pubsub-triggered function back to its caller
Under The Hood
Architecture
The package separates its type-only public surface from its runtime logic across a handful of single-responsibility files: definers.ts wraps each handler type with a runtime function check, durables.ts resolves the overloaded createDurable(config?, handler) call shape and validates the config’s name/event.type, and invoke.ts contains the only genuinely stateful logic — a lazily-initialized, promise-cached AWS client (getAwsLite), DynamoDB-backed resource lookup (getResource), a lineage-token builder that increments and caps a recursion counter (buildLineageToken/MAX_RECURSION_COUNT), and payload-size guards before dispatching to SNS, SQS, or Lambda depending on invocation type. Types live in a dedicated types/ tree (functions.ts, context.ts, resources.ts, invocation.ts, durables.ts), keeping the runtime code and its typings independently readable.
Tech Stack
Written in strict TypeScript targeting Node >=22, built with tsc into ESM-only output (type: module), with an exports map that exposes a source condition for tooling alongside compiled dist. AWS integration is delegated entirely to the aws-lite ecosystem (@aws-lite/client plus dynamodb, lambda, sns, and sqs plugins), declared as peer dependencies so consumers who never call invoke() don’t pull them in. Testing runs on Vitest (including its built-in typecheck mode for .test-d.ts type tests), linting/formatting on Biome, API docs generation via Typedoc, and releases are automated with release-please.
Code Quality
Every exported function has matching runtime tests (definers.test.ts, durables.test.ts, invoke.test.ts) and separate .test-d.ts files that exercise the type-level API, both run under Vitest’s --typecheck mode. CI goes further than most small SDKs: after npm run prepare it runs a full typecheck, Biome lint, and then packs the tarball and imports the built ESM output plus its emitted .d.ts declarations in a scratch NodeNext project to catch extensionless-import bugs before they reach consumers. Error handling is explicit and typed (throw new TypeError(...)/throw new Error(...) with specific messages) rather than silent failure.
API Design
The handler definers are near-zero-boilerplate: each is effectively an identity function that exists purely to pin the handler’s type and throw on an obvious misuse, so getting started is a single import plus one function definition. invoke() uses overloaded signatures keyed on the sync option so its return type (Promise<T> vs Promise<void>) is inferred automatically rather than requiring a generic cast, and naming is consistent across the four *EventHandler definers, which lowers the API’s cognitive load for anyone who has used one of them.