NestJS Meets LangChain: A Practical Guide to Wrapping vs. Building from Scratch
NestJS + LangChain Integration Guide: From Library Wrapper to Manual Construction
📌 Preface
There are two main ways to integrate LangChain in NestJS:
- Library Wrapper Approach: Use the third-party library
nestjs-langchainto quickly integrate viaLangChainModuleandLangChainService. - Manual Construction Approach: Use core packages like
@langchain/openaidirectly in a Service to manually assemble Chains.
Each approach has its pros and cons. This article will start with the simplest "out-of-the-box" solution and gradually transition to a more flexible and controllable manual construction approach, helping you choose the right one based on your project's needs.
🚀 Phase 1: Library Wrapper Approach (Out-of-the-Box)
1.1 Install Dependencies
npm install nestjs-langchain langchain @langchain/openai
1.2 Register the LangChain Module
Configure globally in AppModule using LangChainModule.register():
// 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', // Specify the model
apiKey: process.env.OPENAI_API_KEY,
},
systemPrompt: 'You are a helpful assistant.',
}),
AiModule,
],
})
export class AppModule {}
1.3 Inject and Use in a Service
Inject LangChainService via the constructor in any Service:
// 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 LangChainService run method
return await this.langChainService.run(question);
}
}
1.4 Create AI Tools (Advanced Feature)
One of the most powerful features of nestjs-langchain: expose any Service method as an AI-callable tool using the @Tool() decorator.
// math.service.ts
import { Injectable } from '@nestjs/common';
import { Tool, ToolParam } from 'nestjs-langchain';
@Injectable()
export class MathService {
@Tool({
description: 'Add two numbers. 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;
}
}
When registering tools, simply pass MathModule into the tools array:
@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 {}
1.5 Pros and Cons of the Library Wrapper Approach
| Pros | Cons |
|---|---|
| ✅ Out-of-the-box, simple configuration, less code | ❌ Limited by the library's API, lower flexibility |
✅ Convenient tool registration, the @Tool() decorator is very elegant |
❌ Support for the latest LangChain features may lag |
| ✅ Complete dependency injection system, deeply integrated with NestJS | ❌ Introduces extra dependencies, increasing project size |
🔧 Phase 2: Manual Construction Approach (Full Control)
When your business scenario requires high customization (such as custom Prompt templates, multi-model switching, complex Chain orchestration), manual construction is the better choice. This approach uses core packages like @langchain/openai directly and assembles Chains within a Service.
2.1 Install Dependencies
npm install @langchain/openai @langchain/core
2.2 Create the AI Module
Use the NestJS CLI to generate the module, controller, and service:
nest g module ai
nest g controller ai
nest g service ai
2.3 Manually Build the Chain (Core Code)
This is the most critical part—manually initializing the LangChain chain in the AiService constructor.
// ai.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import type { Runnable } from '@langchain/core/runnables';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AiService {
// Chain invocation object
private readonly chain: Runnable;
constructor(@Inject(ConfigService) configService: ConfigService) {
// 1. Define the Prompt template
const prompt = PromptTemplate.fromTemplate(
`Please answer the following question: \n\n{query}`
);
// 2. Initialize the ChatOpenAI model (read config from ConfigService)
const model = new ChatOpenAI({
temperature: 0.7,
modelName: configService.get('MODEL_NAME'),
apiKey: configService.get('OPENAI_API_KEY'),
configuration: {
baseURL: configService.get('OPENAI_BASE_URL'),
},
});
// 3. Assemble the Chain (chain using the .pipe() method)
this.chain = prompt.pipe(model).pipe(new StringOutputParser());
}
// 4. Expose the run method for the Controller to call
async runChain(query: string): Promise<string> {
return this.chain.invoke({ query });
}
}
2.4 Register the Module
// ai.module.ts
import { Module } from '@nestjs/common';
import { AiService } from './ai.service';
import { AiController } from './ai.controller';
@Module({
controllers: [AiController],
providers: [AiService],
})
export class AiModule {}
2.5 Write the Controller
// ai.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { AiService } from './ai.service';
@Controller('ai')
export class AiController {
constructor(private readonly aiService: AiService) {}
@Get('chat')
async chat(@Query('query') query: string) {
const answer = await this.aiService.runChain(query);
return {
answer,
};
}
}
2.6 Configure Environment Variables
Add the necessary configuration to the .env file:
OPENAI_API_KEY=your-api-key-here
OPENAI_BASE_URL=https://api.openai.com/v1
MODEL_NAME=gpt-3.5-turbo
2.7 Line-by-Line Analysis of Key Code
| Code Segment | Explanation |
|---|---|
PromptTemplate.fromTemplate() |
Defines the Prompt template, using the {query} placeholder |
new ChatOpenAI({...}) |
Initializes the OpenAI model instance, reading config from ConfigService |
prompt.pipe(model).pipe(new StringOutputParser()) |
Chains using the .pipe() method: Template → Model → Output Parser |
this.chain.invoke({ query }) |
Executes the chain call, passing query to replace the placeholder in the template |
ConfigService |
NestJS built-in config service, reads environment variables from the .env file |
2.8 Pros and Cons of the Manual Construction Approach
| Pros | Cons |
|---|---|
| ✅ Fully controllable, freely customize every step | ❌ Requires manual management of LangChain dependencies and versions |
| ✅ No extra dependencies, only uses LangChain core packages | ❌ Relatively more code, needs self-maintained Chain construction logic |
| ✅ Keeps up with the latest LangChain features, not limited by third-party libraries | ❌ Tool registration requires implementing Agent logic yourself |
| ✅ Easy to test, can mock specific methods |
📊 Comparison Summary of the Two Approaches
| Comparison Dimension | Library Wrapper (nestjs-langchain) |
Manual Construction |
|---|---|---|
| Configuration Complexity | Simple, centralized in AppModule |
Medium, requires self-initialization of Model and Chain |
| Flexibility | Limited by library API | Fully flexible, allows deep customization |
| Tool Registration | ✅ @Tool() decorator, extremely convenient |
❌ Requires manual implementation of Agent logic |
| Learning Curve | Gentle, out-of-the-box | Steeper, requires understanding of LangChain core concepts |
| Dependency Management | One extra nestjs-langchain dependency |
Only depends on @langchain/* core packages |
| Applicable Scenarios | Rapid prototyping, standard Q&A, tool invocation | Complex Prompt engineering, multi-model switching, custom Chains |
🎯 Practical Advice: How to Choose?
Choose the 'Library Wrapper Approach' if:
- You are just starting with NestJS + LangChain and want to get up and running quickly.
- You need the AI to call external tools (like checking the weather, doing math, querying a database), and the
@Tool()decorator can significantly boost development efficiency. - Your business scenario is fairly standard and doesn't require deep customization of Prompts or Chains.
Choose the 'Manual Construction Approach' if:
- You need fine-grained control over Prompt templates (such as dynamic splicing, conditional branching).
- You need to use the latest experimental features of LangChain that
nestjs-langchaindoesn't support yet. - You want to keep your project's dependency tree as lean as possible, avoiding unnecessary third-party libraries.
- You want the ability to easily swap out the model's underlying implementation in future iterations.
🔄 Migrating from Manual Construction to Library Wrapper (or Vice Versa)
If you are currently using the manual construction approach (like the AiService in this article) and want to switch to the library wrapper approach, you just need to:
- Install
nestjs-langchain. - Register
LangChainModuleinAppModule. - Delete the
@Inject(ConfigService)and LangChain initialization code inAiService. - Change the constructor to inject
LangChainService. - Call
this.langChainService.run(question)instead ofthis.chain.invoke({ query }).
Conversely, if you switch from the library wrapper back to manual construction, just reverse the steps above.
🧠 Summary
The library wrapper approach is like "buying a pre-built PC," while manual construction is like "building your own."
A pre-built PC (library wrapper) is out-of-the-box and suitable for most scenarios; building your own (manual construction) takes a bit more effort, but you decide the specs of every component.
As a learning path, I suggest:
- Beginner Phase: First, get the full workflow running with the library wrapper approach to build confidence.
- Advanced Phase: Switch to manual construction to deeply understand LangChain's core concepts (Prompt, Model, Chain, Parser).
- Practical Phase: Based on actual project needs, flexibly choose between the two approaches or even mix them.
Your AiService manual construction code is very standard; you have perfectly mastered the second approach. If you need the AI to call external tools, you can always consider introducing the @Tool() decorator from nestjs-langchain to free your hands! 🚀