Skip to content

Repository files navigation

@nest-native/ai-sdk

Decorator-first NestJS streaming primitive for the Vercel AI SDK that preserves the full Nest enhancer pipeline.

NPM Version Package License Test Coverage

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/.

What This Is

@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.

Why

NestJS integration for AI SDK streaming is missing today:

  • The official cookbook uses raw @Res() + pipeUIMessageStreamToResponse(), which bypasses interceptors, guards, and exception filters.
  • @Sse is 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.

Compatibility

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.

Repository Layout

This repository contains:

  • packages/ai-sdk: the @nest-native/ai-sdk integration package
  • sample: runnable samples, starting with sample/00-showcase
  • MIGRATION.md: step-by-step guide from the official cookbook's raw @Res() + pipe*ToResponse recipe to @AiStream
  • scripts: quality, coverage, complexity, and release-check helpers
  • CONTRIBUTING.md: contributor workflow, including the sample/library PR separation rule
  • CHANGELOG.md: release history and unreleased changes
  • SECURITY.md: vulnerability reporting and project security boundaries
  • website: the Docusaurus documentation site

The published documentation site is the recommended learning path; it is built from website/.

Installation

npm i @nest-native/ai-sdk ai

Required peers:

npm i @nestjs/common @nestjs/core reflect-metadata rxjs

Install the HTTP adapter your app uses:

npm i @nestjs/platform-express
# or @nestjs/platform-fastify

Usage

Register 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.

Testing

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.

Quality Gates

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-save on top of the 11 lockfile (11.0.0 pinned 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 ci

One 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:full re-tests everything). Scope with STRYKER_MUTATE (comma-separated globs).

Details — including the pre-PR ritual and agent instructions — in GUIDELINES_NEST_AI_SDK.md.

Status and Roadmap

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:

  1. Bootstrap — repo skeleton, empty package, CI green. ✅
  2. @AiStream skeleton on Express with a showcase sample. ✅
  3. Fastify parity. ✅
  4. @AiAbortSignal + real disconnect test. ✅
  5. Pre-stream vs in-stream error mapping. ✅
  6. streamObject + streamUI samples. ✅
  7. Migration guide from raw @Res() piping. ✅
  8. Documentation site. Release v0.1. ✅
  9. @AiContext — request-scoped context for tool execute. Release v0.2. ✅
  10. Adopt the AI SDK v6 major (peer ai ^6, samples on zod ^4). Release v0.3. ✅
  11. Adopt the AI SDK v7 major (peer ai ^7, @ai-sdk/provider ^4). Release v0.4. ✅
  12. @nest-native/ai-sdk/testing — deterministic offline mock language models; supported Node.js line raised to >=22 to match the AI SDK's own engines. Release v0.5. ✅

See CHANGELOG.md for what has landed and the roadmap for the scope boundary.

License

MIT © 2026 Rodrigo Nogueira.

Part of the nest-native family, alongside @nest-native/drizzle and @nest-native/trpc.

Releases

Packages

Contributors

Languages