Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 2 min read

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

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 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?

Difference between with structured output and without structured output

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.

  • output is an OrderDraft: output.sku compiles, a typo does not.
  • The input asks for the catalog search of part 2: otherwise the model may write a SKU from memory.
  • POST /ask still 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.

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.