Getting Started
Stand up an AI chat in a NestJS app — install, register the module with a model, declare a tool, and stream your first turn.
The fastest path to a working agent is three steps: install, register AgentModule with a model,
and send a message. Everything else — the store, who the caller is, which tools they may use, the
system prompt — has a default you can override one option at a time.
Prerequisites
- Node.js 20+, NestJS 10+ (v10, v11 and v12 are supported) on the Express platform
- TypeScript 5+ with
experimentalDecoratorsandemitDecoratorMetadataenabled - A model — any Vercel AI SDK v7
LanguageModel(openai('gpt-5-mini'),anthropic('claude-sonnet-4-5'), an AI Gateway id like'openai/gpt-5-mini'), or anything implementing theModelProviderSPI
Step 1 — Install
pnpm add @dudousxd/nestjs-agent @dudousxd/nestjs-agent-ai-sdk @ai-sdk/openai zod@dudousxd/nestjs-agent is the NestJS module (it re-exports the whole -core surface);
@dudousxd/nestjs-agent-ai-sdk adapts an AI SDK model so you write no provider code.
Step 2 — Register the module
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AgentModule } from '@dudousxd/nestjs-agent';
import { aiSdkModel } from '@dudousxd/nestjs-agent-ai-sdk';
import { openai } from '@ai-sdk/openai';
@Module({
imports: [
AgentModule.forRoot({
model: aiSdkModel(openai('gpt-5-mini')),
systemPrompt: 'You are a helpful assistant for Acme.', // optional
}),
],
})
export class AppModule {}That boots. At startup you will see two notices, both about defaults you will want to change before production:
WARN [AgentModule] No `store` configured and no store module imported — using the in-memory store. …
WARN [AgentModule] No `actorResolver` configured — the agent endpoints are PUBLIC: every browser gets its own anonymous identity (an HttpOnly cookie). …Anonymous by default — and what that means
With no actorResolver, anyone who can reach the API can chat, and each browser is its own actor:
the server sets an HttpOnly, SameSite=Lax cookie holding a random token and uses a SHA-256 digest
of it as the actor id. Threads, quota and attachments are scoped by that id, so visitors never see
each other's conversations. To require login, add one line —
actorResolver: requestUserActorResolver() reads req.user (Passport, cookie sessions); see
Identity & Authorization.
Step 3 — Declare a tool (optional)
A tool is a regular injectable decorated with @AiTool. Only description and input are
required: the name defaults to the class name without Tool (GetWeatherTool → getWeather) and
the kind to read, which auto-executes. A kind: 'action' tool never auto-executes — the run pauses
for approval (see Human-in-the-loop).
// src/tools/get-weather.tool.ts
import { AiTool } from '@dudousxd/nestjs-agent';
import { z } from 'zod';
@AiTool({ description: 'Current weather for a city.', input: z.object({ city: z.string() }) })
export class GetWeatherTool {
async execute({ city }: { city: string }) {
return { city, tempC: 21, summary: 'partly cloudy' };
}
}List it in a module's providers (providers: [GetWeatherTool]) and the agent discovers it. Because
a tool is an ordinary provider, it can inject anything else in your app. By default every tool is
callable by every resolved actor; restrict one with @AiTool({ roles: ['ADMIN'] }).
Step 4 — Send a message
The module mounts SSE + REST endpoints under /agent (configurable via path). Start a turn by
POSTing to /agent/chat; the answer streams back as Server-Sent Events:
curl -N -c cookies.txt -b cookies.txt http://localhost:3000/agent/chat \
-H 'content-type: application/json' \
-d '{ "message": "What is the weather in Lisbon?" }'event: meta
data: {"runId":"…","threadId":"…"}
id: 1
data: {"kind":"step-start"}
id: 2
data: {"kind":"tool-input-available","id":"call-1","name":"getWeather","input":{"city":"Lisbon"},"toolKind":"read"}
id: 3
data: {"kind":"tool-output","id":"call-1","output":{"city":"Lisbon","tempC":21,"summary":"partly cloudy"}}
id: 4
data: {"kind":"step-finish"}
id: 5
data: {"kind":"step-start"}
id: 6
data: {"kind":"text","text":"It's partly cloudy and 21°C in Lisbon."}
id: 7
data: {"kind":"step-finish"}
event: done
data: {}The cookie jar keeps your anonymous identity between requests — GET /agent/threads with the same
jar lists the thread you just started. The full frame vocabulary is in
Stream events.
Step 5 — A React client
pnpm add @dudousxd/nestjs-agent-react ai @ai-sdk/reactimport { ChatInput, MessageList, useAgentChat } from '@dudousxd/nestjs-agent-react';
export function Chat() {
const chat = useAgentChat(); // same origin, `/agent`
return (
<>
<MessageList messages={chat.messages} status={chat.status} />
<ChatInput onSubmit={(text) => chat.sendMessage({ text })} />
</>
);
}Continue with Hello chat (the smallest end-to-end app) and Full-featured chat (threads, models, attachments, approvals, quota).
The defaults, and how to change each
| Default | Override |
|---|---|
| In-memory store — lost on restart, not shared across processes | store, or import a store module (MikroOrmAgentStoreModule.forFeature(), DrizzleAgentStoreModule.forRoot({ db })) — found automatically |
| Public endpoints, one anonymous actor per browser | actorResolver: requestUserActorResolver(), or your own ActorResolver |
| Every tool callable by any resolved actor | @AiTool({ roles }), defaultRoles, or a rolesPolicy |
'You are a helpful assistant.' | systemPrompt (string or ({ actor, agentName, pageContext }) => string), or @Agent classes |
| One model, no picker | model: aiSdkModels({ fast: …, smart: … }, { default: 'fast' }) |
| Usage reported, never enforced | quota: { limits: { day: { tokens: 200_000 }, month: { usd: 20 } } } |
| Turns run in-process | durable: true + AgentDurableModule (Runners) |
An action call holds its turn until someone decides | actionApprovalMode: 'independent' + a backgroundActorResolver — the turn finishes and the call waits as a proposal (Action proposals) |
Routes under /agent | path: 'api/agent' (and <AgentProvider path="api/agent"> on the client) |
Before you go public
An anonymous deployment without quota limits lets anyone spend your model budget. Set limits —
they apply per actor, which in anonymous mode is per browser — or require login.
Try it offline
@dudousxd/nestjs-agent-testing ships a deterministic fake model (FakeModelProvider), so the
whole loop — auto-executing reads, a suspending action tool, multi-agent delegation — runs with no
API key. See Offline testing; the repo's examples/agent-demo
is a runnable app.
Next steps
- Hello chat — server in ~6 lines, React in ~5
- Full-featured chat —
AgentProvider+useAgentChatwith everything on - Tools — the
@AiToolsurface,readvsaction, the handler context - Identity & Authorization — anonymous mode, login, roles
- Persistence — the
AgentStoreon MikroORM or Drizzle - Migrating — every API the DX release removed, and its replacement
Agent
A governed AI agent for NestJS — chat, tool-calling, per-tool RBAC, token quota, cost tracking, and human-in-the-loop approval, with each turn optionally a replay-safe durable workflow and wired into the ecosystem's durable, telescope, and diagnostics glue.
Hello chat
The smallest end-to-end AI chat — a NestJS server in about six lines and a React client in about five, no login, no database.