LangChain Structured Output in NestJS: Typed with Zod
To get an object instead of text from a LangChain agent in NestJS, pass a Zod schema as output to run(). This tutorial turns a customer message into an order your code can use, in a dozen lines. This is part 3 of AI age
To get an object instead of text from a LangChain agent in NestJS, pass a Zod schema as output to run(). This tutorial turns a customer message into an order your code can use, in a dozen lines.
This is part 3 of AI agents with NestJS. Part 2 gives the agent the catalog tools used here.
Tested with NestJS 12, LangChain 1.x, Zod 4, nestjs-langchain 2.1 and Node 22.
What does structured output change?
Same message, same model. Without a schema, the agent answers in text, and your code has to dig the SKU out of a sentence. With one, it answers an object, checked against the schema and typed by it.
The model is still not deterministic: it decides what goes in each field. But the shape of its answer is fixed, and that is what your backend relies on.
How do I describe the answer?
With a Zod object, in orders/order-draft.schema.ts:
import { z } from 'zod';
export const OrderDraft = z.object({
sku: z.string().describe('The SKU of the product in the catalog.'),
quantity: z.number().int().positive(),
});
export type OrderDraft = z.infer<typeof OrderDraft>;
The model receives this schema, descriptions included, and fills in each field from them. .transform() and .refine() work too: see docs/structured-output.md.
How do I get it from run()?
Pass the schema as output, in a new orders/orders.controller.ts:
import { Body, Controller, Post } from '@nestjs/common';
import { Agent } from 'nestjs-langchain';
import { OrderDraft } from './order-draft.schema.js';
@Controller('orders')
export class OrdersController {
constructor(private readonly agent: Agent) {}
@Post('draft')
async draft(@Body('message') message: string): Promise<OrderDraft> {
const { output } = await this.agent.run(
`Find the product of this customer message in the catalog, then draft the order.\n\n${message}`,
{ output: OrderDraft },
);
return output;
}
}
Then add OrdersController to the controllers of AppModule.
-
outputis anOrderDraft:output.skucompiles, a typo does not. - The input asks for the catalog search of part 2: otherwise the model may write a SKU from memory.
-
POST /askstill answers in text: the schema belongs to the run, not to the agent.
Try it
npm run start:dev
curl -X POST http://localhost:3000/orders/draft \
-H 'Content-Type: application/json' \
-d '{"message": "Two K2 keyboards, please."}'
{ "sku": "KB-101", "quantity": 2 }
The model found the SKU with the searchProducts tool of part 2, then answered through the schema.
Next: test it without an API key
The agent now answers in a shape your code can trust. Part 4 tests all of it with LangChain's fakeModel(), which replays the tool calls and answers you queue: the catalog tools, this endpoint, the retry and the 422, with no provider and no network. The tests are already in the repo, in orders/orders.controller.spec.ts.
- The code of this article: nestjs-ai-agent-example, tag part-3
- Structured output in detail, and its limits: docs/structured-output.md
- Package: nestjs-langchain on npm
- Source, issues and stars: GitHub
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.