Decorator-first NestJS streaming primitive for the Vercel AI SDK that preserves the full Nest enhancer pipeline.
Note
Status: 0.6.0 (0.x). Functional and fully tested (100% coverage), and
usable today — but the public API may still change before 1.0, so pin a
version (per semver, 0.x minor releases can include breaking changes). See the
support policy.
@AiStream streams AI SDK results on
both Express and Fastify while preserving the full Nest enhancer pipeline,
@AiAbortSignal cancels the AI SDK call when the client disconnects mid-stream,
@AiContext injects request-scoped context ({ request, response, signal }) so
an AI SDK tool's execute closure can reach the current request mid-stream,
pre-stream vs in-stream errors are mapped correctly, and samples cover
streamText, streamObject, the v5 generative-UI equivalent of streamUI, and
request-scoped tool context. The workspace builds, typechecks, tests at 100%
coverage, and is CI-green. A migration guide ports the official
AI SDK NestJS cookbook recipe to @AiStream, and a
documentation site is published from
website/.
@nest-native/ai-sdk is a community NestJS integration for streaming responses
from the Vercel AI SDK. The goal is a decorator-first,
Nest-native primitive that replaces the "raw @Res() + manual piping" pattern
the official AI SDK cookbook recommends — while keeping the full Nest enhancer
pipeline (guards, pipes, interceptors, filters) intact.
It is the only Nest-native primitive aimed at AI SDK streaming. It wraps the AI SDK's response helpers; it does not re-implement or hide the AI SDK.
NestJS integration for AI SDK streaming is missing today:
- The official cookbook uses raw
@Res()+pipeUIMessageStreamToResponse(), which bypasses interceptors, guards, and exception filters. @Sseis structurally broken for this use case: it opens the connection before the handler runs (nestjs/nest#12670), so pre-flight auth errors become SSE error events instead of HTTP errors.
This package's headline differentiators:
- Pre-stream guard semantics: rejections become HTTP errors (401/403), not SSE error frames.
- Real, tested AbortSignal propagation: client disconnect cancels the underlying AI SDK call so billing stops.
- Express + Fastify parity is a shipped goal, not an assumption.
| Runtime | Supported line |
|---|---|
| Node.js | >=22 (required by ai@7; >=22.12 with NestJS 12 — see the note below the table) |
NestJS (@nestjs/common, @nestjs/core peers) |
^11.0.0 || ^12.0.0 |
Vercel AI SDK (ai) |
^7 (tracks the current major; older majors not supported) |
| HTTP adapter | Express and Fastify (parity is a project goal) |
| Validation | Zod and class-validator, both app-owned |
The published package keeps "dependencies": {}. The Vercel AI SDK and the
NestJS packages are declared as peerDependencies, so applications install only
the ecosystems they actually use.
Both ends of that range are exercised in CI, not just declared: the default
install tests the lockfile's 11.x (the devDependencies and the lockfile stay
on 11 on purpose), and the nestjs-compat matrix installs each end on top of
it with --no-save — 11.0.0 pinned exactly, with @nestjs/platform-fastify
at 11.0.2, the first fastify release whose peers admit 11, and ^12 —
proves every workspace, the package and all eight samples, resolves exactly
that, and runs the suite and the full sample matrix against it.
The Node.js floor depends on which end of that range you are on. NestJS 11 runs
on any Node.js >=22. NestJS 12 ships ESM-only, and a CommonJS application
(the samples here run ts-node in CommonJS mode) loads it through Node's
require(esm), which is behind a flag before Node.js 22.12.0 — so the 12 end
of the range needs Node.js >=22.12. engines stays >=22 because the 11
end does not need more, and the @nestjs/*@12 packages' own engines field
(>= 20) does not encode that floor, so npm will not warn you: run NestJS 12
on a current Node 22 or 24.
This repository contains:
packages/ai-sdk: the@nest-native/ai-sdkintegration packagesample: runnable samples, starting withsample/00-showcaseMIGRATION.md: step-by-step guide from the official cookbook's raw@Res()+pipe*ToResponserecipe to@AiStreamscripts: quality, coverage, complexity, and release-check helpersCONTRIBUTING.md: contributor workflow, including the sample/library PR separation ruleCHANGELOG.md: release history and unreleased changesSECURITY.md: vulnerability reporting and project security boundarieswebsite: the Docusaurus documentation site
The published documentation site is the
recommended learning path; it is built from website/.
npm i @nest-native/ai-sdk aiRequired peers:
npm i @nestjs/common @nestjs/core reflect-metadata rxjsInstall the HTTP adapter your app uses:
npm i @nestjs/platform-express
# or @nestjs/platform-fastifyRegister AiModule, then decorate a handler with @AiStream. The handler
returns an AI SDK stream result (for example from streamText) and the
decorator pipes it to the active HTTP adapter — guards, pipes, interceptors, and
exception filters all run first.
import { Module } from '@nestjs/common';
import { AiModule } from '@nest-native/ai-sdk';
@Module({
imports: [
AiModule.forRoot({
defaultHeaders: { 'x-powered-by': 'nest-native-ai-sdk' },
}),
],
})
export class AppModule {}import { AiStream } from '@nest-native/ai-sdk';
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { streamText } from 'ai';
@Controller('chat')
export class ChatController {
@Post()
@AiStream()
@UseGuards(ApiKeyGuard)
chat(@Body() body: ChatDto) {
// The guard runs before the stream opens — a rejection is HTTP 401/403,
// never an SSE error frame.
return streamText({ model, prompt: body.prompt });
}
}See sample/00-showcase for a full Express example wiring
a guard, a Zod pipe, an interceptor, and an exception filter around @AiStream.
Async configuration is supported through AiModule.forRootAsync():
AiModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
defaultHeaders: { 'x-app': config.getOrThrow('APP_NAME') },
}),
});Both registrations return a global DynamicModule by default. Pass
isGlobal: false to scope it to a single module boundary.
The @nest-native/ai-sdk/testing entrypoint ships the deterministic, offline
mock language models every sample and e2e test in this repository streams from
— no provider, no API keys:
import { createMockLanguageModel } from '@nest-native/ai-sdk/testing';
import { streamText } from 'ai';
const result = streamText({
model: createMockLanguageModel({ text: 'You said: ping' }),
prompt: 'ping',
});createMockLanguageModel streams text as AI SDK v4 protocol chunks (word
deltas for a string, one delta per element for a string[]), fails
mid-stream with the documented in-stream error frame when error is set, and
— with respectAbortSignal: true and a chunkDelayInMs — honors doStream's
abort signal the way a real provider does, exposing capturedSignal() /
started() / settled() observers so a test can prove a client disconnect
cancelled the model call. createToolCallingModel(toolName) emits a single
tool call so a tool execute closure runs. See the
testing documentation for
the full surface. The entrypoint adds no runtime dependencies.
The repository ships the same review posture as its sibling @nest-native
packages, using node:test and c8:
- package build, typecheck, and coverage on Node.js 22 (the supported line)
- NestJS compatibility matrix (
nestjs-compat): installs each end of the published peer range with--no-saveon top of the 11 lockfile (11.0.0pinned exactly, and^12), fails unless every workspace resolves exactly that version with every NestJS-ecosystem peer range satisfied (scripts/check-nestjs-resolution.mjs), then runs the suite and the full sample matrix against it - coverage with
c8, enforced at 100% for statements, branches, functions, and lines - sticky PR comments for coverage, test performance, and cognitive complexity
- cognitive complexity enforcement with SonarJS threshold
15 - package tarball validation and README link validation
- supply-chain audit for high-severity issues
Run the local gate with:
npm run ciOne optional, local-only layer sits on top (it never runs in CI, and forks work without it):
- Mutation testing —
npm run test:mutation(incremental Stryker run;test:mutation:fullre-tests everything). Scope withSTRYKER_MUTATE(comma-separated globs).
Details — including the pre-PR ritual and agent instructions — in GUIDELINES_NEST_AI_SDK.md.
The 0.x line covers the surface below (the public API may still change before
1.0, so pin a version). The v1 decorator surface — @AiStream,
@AiAbortSignal, @AiContext — has been complete since the v0.2 release; the
releases since then tracked the Vercel AI SDK's majors and added the offline
testing entrypoint, without changing that decorator surface. The path that got
here:
Bootstrap — repo skeleton, empty package, CI green.✅✅@AiStreamskeleton on Express with a showcase sample.Fastify parity.✅✅@AiAbortSignal+ real disconnect test.Pre-stream vs in-stream error mapping.✅✅streamObject+streamUIsamples.Migration guide from raw✅@Res()piping.Documentation site. Release✅v0.1.✅@AiContext— request-scoped context for toolexecute. Releasev0.2.Adopt the AI SDK✅v6major (peerai ^6, samples onzod ^4). Releasev0.3.Adopt the AI SDK✅v7major (peerai ^7,@ai-sdk/provider ^4). Releasev0.4.✅@nest-native/ai-sdk/testing— deterministic offline mock language models; supported Node.js line raised to>=22to match the AI SDK's own engines. Releasev0.5.
See CHANGELOG.md for what has landed and the roadmap for the scope boundary.
MIT © 2026 Rodrigo Nogueira.
Part of the nest-native family, alongside @nest-native/drizzle and @nest-native/trpc.