NestJS Meets LangChain: A Structured Blueprint for AI Backends
From NestJS Basics to LangChain in Practice: Building a Maintainable AI Application Architecture
Start your AI application development journey with the most elegant Node.js framework.
Preface
If you are learning NestJS + LangChain, congratulations — you are on the right path to writing high-quality backend code while embracing the AI wave. NestJS provides a rigorous architectural specification, and LangChain makes it easy to integrate large language model capabilities.
But before diving into AI, many learners find that the basic concepts of NestJS have become a bit fuzzy — what is the relationship between Controller, Service, and Module? What exactly is the difference between Pipes and Interceptors? This guide is prepared for you.
I will help you rebuild the core skeleton of NestJS, and then smoothly transition into the AI world of LangChain.
1. Why NestJS? How is it different from Express?
Before writing code, let's clarify a fundamental question: why does the industry choose NestJS instead of using Express directly?
| Feature | Express.js | NestJS |
|---|---|---|
| Architectural Style | Minimalist, no fixed pattern | Modular, layered architecture |
| TypeScript Support | Requires manual configuration | Native support, out of the box |
| Dependency Injection (DI) | Requires manual dependency management | Built-in powerful DI container |
| Code Organization | Left to the developer's discretion | Enforces organizational structure through the module system |
| Maintainability | Flexible for small projects, easily becomes chaotic for large ones | Naturally suited for large enterprise-level applications |
In short: Express gives you freedom, NestJS gives you standards. When you are building an AI application that requires long-term maintenance and multi-person collaboration, the architectural advantages of NestJS become fully apparent.
2. Quick Start: Setting Up Your First NestJS Project
# Install NestJS CLI
npm i -g @nestjs/cli
# Create a project (enable TypeScript strict mode)
nest new my-ai-app --strict
# Enter the project directory
cd my-ai-app
# Start the development server
npm run start:dev
Visit http://localhost:3000, and you will see the classic "Hello World!".
The created project structure is as follows:
my-ai-app/
├── src/
│ ├── main.ts # Application entry point, responsible for starting the service
│ ├── app.module.ts # Root module, the "commander-in-chief" of the application
│ ├── app.controller.ts # Root controller, handles HTTP requests
│ └── app.service.ts # Root service, carries business logic
├── test/ # Test files
├── nest-cli.json # Nest CLI configuration
├── package.json
└── tsconfig.json # TypeScript configuration
3. Three Core Concepts: Modules, Controllers, Services
These are the cornerstones of NestJS. Understanding these three builds the skeleton of your NestJS knowledge.
3.1 Modules — The "Departments" of the Application
Modules use the @Module() decorator to organize code. Every application has at least one root module, AppModule.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [], // Import other modules (e.g., database module, AI module)
controllers: [AppController], // Register the module's controllers
providers: [AppService], // Register the module's services/providers
exports: [], // Export services for use by other modules
})
export class AppModule {}
Modules are the basic unit for organizing code in NestJS. You can divide modules by functional domain, such as UserModule, OrderModule, AiModule.
3.2 Controllers — The "Front Desk" Handling HTTP Requests
Controllers are responsible for receiving requests for specific routes and returning responses. They only do "route forwarding" and do not write business logic.
// src/app.controller.ts
import {
Controller, Get, Post, Put, Delete,
Body, Param, Query, HttpCode, HttpStatus
} from '@nestjs/common';
@Controller('users') // Route prefix is /users
export class AppController {
constructor(private readonly appService: AppService) {}
@Get() // Handles GET /users
getUsers() {
return this.appService.getUsers();
}
@Get(':id') // Handles GET /users/123
getUserById(@Param('id') id: string) {
return this.appService.getUserById(id);
}
@Post() // Handles POST /users
@HttpCode(HttpStatus.CREATED) // Returns 201 status code
createUser(@Body() createUserDto: any) {
return this.appService.createUser(createUserDto);
}
@Get('search') // Handles GET /users/search?keyword=nest
searchUsers(@Query('keyword') keyword: string) {
return this.appService.searchUsers(keyword);
}
@Put(':id')
updateUser(@Param('id') id: string, @Body() updateUserDto: any) {
return this.appService.updateUser(id, updateUserDto);
}
@Delete(':id')
deleteUser(@Param('id') id: string) {
return this.appService.deleteUser(id);
}
}
Parameter Decorator Quick Reference
| Decorator | Data Location Extracted | Example | Use Case |
|---|---|---|---|
@Param() |
URL path variable | /users/123 extracts 123 |
Get a specific resource ID |
@Query() |
URL ? parameters | ?page=1&size=10 |
Pagination, filtering, searching |
@Body() |
Request body (JSON/Form) | { name: 'Xiao Ming' } |
Create/Modify resources |
3.3 Services — The "Employees" Carrying Business Logic
Services are a type of provider, using the @Injectable() decorator. They contain the core business logic, and controllers use services through Dependency Injection (DI).
// src/app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable() // Marks it as an injectable provider
export class AppService {
private users = [{ id: '1', name: 'John' }];
getUsers() {
return this.users;
}
getUserById(id: string) {
return this.users.find(user => user.id === id);
}
createUser(createUserDto: any) {
const newUser = { id: String(this.users.length + 1), ...createUserDto };
this.users.push(newUser);
return newUser;
}
searchUsers(keyword: string) {
return this.users.filter(user => user.name.includes(keyword));
}
updateUser(id: string, updateUserDto: any) {
const user = this.getUserById(id);
Object.assign(user, updateUserDto);
return user;
}
deleteUser(id: string) {
this.users = this.users.filter(user => user.id !== id);
return { deleted: true };
}
}
3.4 The Relationship Between the Three: Explained in One Diagram
┌─────────────────────────────────────────────────────────┐
│ Module │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ Controller │ │ Service │ │
│ │ - Receives HTTP │───→│ - Core business logic │ │
│ │ requests │ │ - Data validation │ │
│ │ - Parameter │ │ - Database operations │ │
│ │ parsing │ │ │ │
│ │ - Returns │ │ │ │
│ │ responses │ │ │ │
│ └─────────────────┘ └──────────────────────────┘ │
│ │ │ │
│ │ Dependency Injection │ │
│ └───────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
4. Generate Code with CLI to Boost Development Efficiency
The NestJS CLI can quickly generate files for modules, controllers, services, etc., greatly improving development efficiency:
# Generate a users module (creates users.module.ts)
nest g module users
# Generate a users controller (creates users.controller.ts and test file)
nest g controller users
# Generate a users service (creates users.service.ts and test file)
nest g service users
# Generate a complete CRUD resource with one command (module + controller + service)
nest g resource products
💡 Practical Advice: When you need to add a new feature module, always use the nest g command to generate the skeleton first, rather than manually creating files. This not only ensures a standard code structure but also automatically updates the module's dependency registration.
5. Pipes: The "Security Gate" and "Translator" for Data
Pipes work before the controller executes, with two core functions: Validation and Transformation.
5.1 Standard Three-Step Process for Combining Pipes and DTOs
Step 1: Install Dependencies
npm install class-validator class-transformer
Step 2: Attach "Rule Labels" in the DTO Class
// src/users/dto/create-user.dto.ts
import {
IsString, IsEmail, IsInt, IsNotEmpty,
Min, MaxLength, IsOptional
} from 'class-validator';
import { Transform } from 'class-transformer';
export class CreateUserDto {
@IsString()
@IsNotEmpty({ message: 'Name cannot be empty' })
@MaxLength(50, { message: 'Name cannot exceed 50 characters' })
name: string;
@IsEmail({}, { message: 'Please enter a valid email format' })
@IsNotEmpty()
email: string;
@IsInt()
@Min(18, { message: 'Age must be greater than 18' })
@Transform(({ value }) => parseInt(value, 10)) // Automatically convert string "18" to number 18
age: number;
@IsOptional() // Optional field
@IsString()
bio?: string;
}
Step 3: Use in the Controller
// src/users/users.controller.ts
import { Body, Post, UsePipes, ValidationPipe } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Post()
// Method 1: Local activation (most common, recommended)
async createUser(@Body(ValidationPipe) createUserDto: CreateUserDto) {
// By the time execution reaches here, the data has passed validation and type conversion is complete
return this.usersService.create(createUserDto);
}
}
5.2 Enable Globally (Recommended)
Enable the global validation pipe in main.ts to automatically apply it to all endpoints:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable global validation pipe
app.useGlobalPipes(new ValidationPipe({
transform: true, // Automatic type conversion
whitelist: true, // Strip fields not defined in the DTO
forbidNonWhitelisted: true, // Throw an error if undefined fields exist
stopAtFirstError: true, // Stop validation on the first error encountered
}));
await app.listen(3000);
}
bootstrap();
💡 Practical Significance for LangChain: A user's question must not be empty, and the token count must be legal. Using pipes to intercept illegal requests can prevent wasting your AI quota on invalid calls.
6. Middleware, Guards, Interceptors: Three Checkpoints on the Request Chain
6.1 Execution Order Diagram (Core)
Request In → Middleware → Guards → Interceptors (pre) → Pipes → Controller → Service → Interceptors (post) → Response Out
6.2 Middleware — The Earliest "Gatekeeper"
Execution Timing: Before all other mechanisms, the first to touch the request.
Main Uses: Logging, setting request headers, CORS configuration, parsing raw body.
// src/common/middleware/logger.middleware.ts
export function loggerMiddleware(req, res, next) {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
console.log('Request Headers:', req.headers);
next(); // Must call next() to pass control
}
// Register in the module
// src/app.module.ts
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(loggerMiddleware)
.forRoutes('*'); // Apply to all routes, or specify specific paths
}
}
6.3 Guards — The Permission "Security Inspector"
Execution Timing: After middleware, before interceptors/pipes.
Core Function: Determine if the current request has permission to access this route. Returns true to allow, returns false or throws an exception to block.
// src/common/guards/api-key.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const apiKey = request.headers['x-api-key'];
if (apiKey && apiKey === process.env.API_SECRET_KEY) {
return true;
}
throw new UnauthorizedException('Invalid API Key');
}
}
// Use in a controller
@Post('ask')
@UseGuards(ApiKeyGuard) // Only requests with the correct Key can enter
async askQuestion(@Body(ValidationPipe) dto: AskQuestionDto) {
return this.aiService.ask(dto);
}
6.4 Interceptors — The Response "Packaging Master"
Execution Timing: Executes pre-logic after guards; executes post-logic after the controller finishes.
Core Functions:
- Pre: Can modify request data
- Post: Uniformly format return results, record execution time, cache responses
// src/common/interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Pre-logic (before controller execution)
console.log('Request processing started...');
return next.handle().pipe(
map(data => ({
code: 200,
success: true,
timestamp: new Date().toISOString(),
data: data, // The controller's return value is wrapped here
}))
);
}
}
// Register globally in main.ts
app.useGlobalInterceptors(new TransformInterceptor());
6.5 Comparison Summary Table
| Feature | Middleware | Guards | Pipes | Interceptors |
|---|---|---|---|---|
| Execution Order | 1st | 2nd | 4th | 3rd (wraps the whole process) |
| Core Responsibility | Raw request handling | Auth/Permission verification | Data validation & transformation | Response mapping & caching |
| Can Access DTO? | ❌ Only gets raw body | ❌ Usually only gets headers/token | ✅ Specifically manages DTO objects | ⚠️ Can get the final result |
| Common Decorator | configure() registration |
@UseGuards() |
@UsePipes() |
@UseInterceptors() |
7. MVC Architecture Implementation in NestJS
Although NestJS does not strictly follow classic MVC, its layered thinking is in the same vein. The Model in traditional MVC is broken down into three parts in NestJS:
| Layer | File Location | Core Responsibility |
|---|---|---|
| Controller Layer | *.controller.ts |
Receive requests, route forwarding, return responses |
| Service Layer | *.service.ts |
Core business logic (calculations, calling external APIs, orchestrating processes) |
| DTO/Entity Layer | *.dto.ts / *.entity.ts |
Data structure definition + data validation rules |
| Repository Layer | Provided by ORM | Database CRUD operations |
┌─────────────────────────────────────────────────────────────┐
│ Controller Layer │
│ (Responsible for routing and parameter parsing) │
└─────────────────────────┬───────────────────────────────────┘
│ Calls
▼
┌─────────────────────────────────────────────────────────────┐
│ Service Layer │
│ (Responsible for core business logic) │
└─────────────────────────┬───────────────────────────────────┘
│ Calls
▼
┌─────────────────────────────────────────────────────────────┐
│ Repository / ORM Layer │
│ (Responsible for database operations) │
└─────────────────────────────────────────────────────────────┘
8. Relationships Between Modules: Not Just 1:1
Beginners easily fall into the misconception that "one module can only have one controller and one service". In reality:
// ✅ One module can contain multiple controllers
@Module({
controllers: [UserController, OrderController, AdminController],
providers: [UserService, OrderService, AdminService],
})
export class AppModule {}
// ✅ One controller can inject multiple services
@Controller('orders')
export class OrderController {
constructor(
private orderService: OrderService,
private inventoryService: InventoryService,
private pointsService: PointsService,
) {}
}
// ✅ A module can also have no controllers (pure utility module)
@Module({
providers: [DatabaseService, LoggerService],
exports: [DatabaseService, LoggerService], // Export for use by other modules
})
export class CommonModule {}
💡 Practical Advice: Divide modules by "functional domain" rather than rigidly 1:1:
AiModule (AI Functionality Module)
├── ChatController → Handles /ai/chat conversations
├── DocumentController → Handles /ai/document document parsing
├── LangChainService → Core LangChain invocation logic
├── VectorService → Vector database operations
└── PromptService → Prompt template management
9. Integrating with LangChain: Let AI Integrate into Your Architecture
Now, string together all the previous knowledge to build a real AI application.
9.1 Install Dependencies
npm install nestjs-langchain langchain @langchain/openai
9.2 Register the LangChain Module
// src/app.module.ts
import { Module } from '@nestjs/common';
import { LangChainModule } from 'nestjs-langchain';
import { AiModule } from './ai/ai.module';
@Module({
imports: [
LangChainModule.register({
model: {
model: 'openai:gpt-3.5-turbo',
apiKey: process.env.OPENAI_API_KEY,
},
systemPrompt: 'You are a helpful AI assistant built with NestJS.',
}),
AiModule,
],
})
export class AppModule {}
9.3 Using LangChain in a Service
// src/ai/ai.service.ts
import { Injectable } from '@nestjs/common';
import { LangChainService } from 'nestjs-langchain';
@Injectable()
export class AiService {
constructor(private readonly langChainService: LangChainService) {}
async askQuestion(question: string) {
// Directly call the AI agent
return await this.langChainService.run(question);
}
async askWithContext(question: string, context: string) {
// Q&A with context (suitable for RAG scenarios)
const prompt = `Answer the question based on the following context:\n\nContext: ${context}\n\nQuestion: ${question}`;
return await this.langChainService.run(prompt);
}
}
9.4 Defining Service Methods as AI Tools — The Most Powerful Feature
This is the most stunning feature of nestjs-langchain: Through the @Tool() decorator, you can turn any method of a NestJS service into a tool that the AI can call.
// src/math/math.service.ts
import { Injectable } from '@nestjs/common';
import { Tool, ToolParam } from 'nestjs-langchain';
@Injectable()
export class MathService {
@Tool({
description: 'Add two numbers together. Use this tool when the user needs to perform addition.'
})
add(
@ToolParam({ name: 'a', description: 'The first addend', type: 'number' })
a: number,
@ToolParam({ name: 'b', description: 'The second addend', type: 'number' })
b: number,
): number {
return a + b;
}
@Tool({
description: 'Get the current weather information, supports querying by city name.'
})
async getWeather(
@ToolParam({ name: 'city', description: 'City name', type: 'string' })
city: string,
): Promise<string> {
// A real weather API could be called here
return `The weather in ${city} is sunny, 25°C`;
}
}
Register the tools:
// src/app.module.ts
@Module({
imports: [
LangChainModule.register({
model: { model: 'openai:gpt-3.5-turbo', apiKey: process.env.OPENAI_API_KEY },
systemPrompt: 'You are an intelligent assistant that can use tools to help users.',
tools: [MathModule], // Register all @Tool() methods in MathModule
}),
MathModule,
],
})
export class AppModule {}
Now, when a user asks "Help me calculate what 123 plus 456 equals", the AI will automatically recognize and call the MathService.add() method.
10. Practical Advice for AI Applications
Based on all the knowledge above, it is recommended to follow these best practices when building AI applications:
1. Protect Your AI Endpoints with Guards
@Post('ask')
@UseGuards(ApiKeyGuard, RateLimitGuard) // Dual protection: API Key + Rate Limiting
async ask(@Body(ValidationPipe) dto: AskDto) {
return this.aiService.ask(dto);
}
2. Validate User Input with Pipes
export class AskDto {
@IsString()
@IsNotEmpty({ message: 'Question cannot be empty' })
@MaxLength(2000, { message: 'Question cannot exceed 2000 characters' })
question: string;
@IsOptional()
@IsInt()
@Min(1)
@Max(10)
temperature?: number; // AI temperature parameter
}
3. Unify Response Format with Interceptors
// All AI endpoints return a unified format
{
code: 200,
success: true,
timestamp: '2026-08-10T10:00:00.000Z',
data: { answer: '...', tokensUsed: 150 }
}
4. Clear Layering, Each with Its Own Role
Controller Layer: Only responsible for routing and parameter validation
Service Layer: Responsible for Prompt construction, LangChain calls, result processing
Repository Layer: Responsible for persisting conversation history and user data
11. Summary
Through this article, we started from scratch and comprehensively reviewed the core concepts of NestJS:
- Three Core Concepts: Modules (organization), Controllers (routing), Services (business logic)
- Parameter Decorators: Distinction and use of
@Param(),@Query(),@Body() - Pipes and DTOs: Standard practice for data validation and transformation
- Request Chain: The complete execution order of Middleware → Guards → Interceptors → Pipes → Controller
- Module Relationships: Not limited to 1:1, flexibly organized by functional domain
- LangChain Integration: Defining NestJS service methods as AI tools
The destination of this journey is your ability to use NestJS to build AI applications that are clearly structured, easy to maintain, secure, and reliable.
Next Steps for Learning
- Deep Dive into LangChain: Understand concepts like Chain, Agent, Retriever, Vector Store
- RAG in Practice: Combine vector databases (Chroma, Pinecone) to implement knowledge base Q&A
- Streaming Responses: Use SSE or WebSocket to implement AI streaming output
- Unit Testing: Use NestJS testing tools to test your AI services
📌 Core code examples have been uploaded, welcome to reference them in actual projects. If you encounter problems, it is recommended to consult the NestJS Official Documentation and LangChain JS Documentation.
Happy Coding! 🚀