You can use Lightfast agents with NestJS, a progressive Node.js framework for building efficient and scalable server-side applications.
Installation
Install NestJS and required dependencies:
npm install @nestjs/core @nestjs/common @nestjs/platform-express @nestjs/jwt @nestjs/throttler
# or
yarn add @nestjs/core @nestjs/common @nestjs/platform-express @nestjs/jwt @nestjs/throttler
# or
pnpm add @nestjs/core @nestjs/common @nestjs/platform-express @nestjs/jwt @nestjs/throttler
Examples
The examples start a NestJS server that listens on port 8080. You can test it using curl
:
curl -X POST http://localhost:8080/agents/my-session \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-jwt-token" \
-d '{"messages":[{"role":"user","content":"What'\''s the weather like?"}]}'
The examples use the OpenAI gpt-4o
model. Ensure that the OpenAI API key is set in the OPENAI_API_KEY
environment variable.
Full example: Available in our examples repository
Setup
First, create your agent and memory configuration:
// config/agent.config.ts
import { createAgent } from 'lightfast/agent';
import { createTool } from 'lightfast/tool';
import { RedisMemory } from 'lightfast/memory/adapters/redis';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
// Create a simple tool
const weatherTool = createTool({
name: 'get_weather',
description: 'Get weather for a location',
parameters: z.object({
location: z.string().describe('The location to get weather for')
}),
execute: async ({ location }) => {
return `Weather in ${location}: Sunny, 72°F`;
}
});
// Create the agent
export const agent = createAgent({
name: 'weather-assistant',
model: openai('gpt-4o'),
system: 'You are a helpful weather assistant.',
tools: { weather: weatherTool },
createRuntimeContext: ({ sessionId, resourceId }) => ({
timestamp: Date.now(),
framework: 'nestjs'
})
});
// Create memory adapter
export const memory = new RedisMemory({
url: process.env.REDIS_URL!,
token: process.env.REDIS_TOKEN!
});
Basic Integration
Service Layer
// services/agent.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { fetchRequestHandler } from 'lightfast/server/adapters/fetch';
import { agent, memory } from '../config/agent.config';
@Injectable()
export class AgentService {
private readonly logger = new Logger(AgentService.name);
async handleAgentRequest(
sessionId: string,
req: Request,
resourceId: string,
additionalContext?: Record<string, any>
): Promise<Response> {
this.logger.log(`Processing agent request for session: ${sessionId}`);
try {
const response = await fetchRequestHandler({
agent,
sessionId,
memory,
req,
resourceId,
createRequestContext: (req) => ({
userAgent: req.headers.get('user-agent'),
nestjs: true,
...additionalContext
}),
onError: ({ error }) => {
this.logger.error(`Agent error for session ${sessionId}:`, error);
}
});
return response;
} catch (error) {
this.logger.error(`Failed to process agent request:`, error);
throw error;
}
}
async getSessionHistory(sessionId: string): Promise<any[]> {
try {
return await memory.getMessages(sessionId);
} catch (error) {
this.logger.error(`Failed to retrieve session history:`, error);
throw error;
}
}
async deleteSession(sessionId: string): Promise<void> {
try {
await memory.deleteSession(sessionId);
this.logger.log(`Session ${sessionId} deleted successfully`);
} catch (error) {
this.logger.error(`Failed to delete session:`, error);
throw error;
}
}
}
Controller
// controllers/agents.controller.ts
import {
Controller,
Post,
Get,
Delete,
Req,
Res,
Param,
Body,
UseGuards,
Logger,
HttpStatus,
HttpException
} from '@nestjs/common';
import { Request, Response } from 'express';
import { AgentService } from '../services/agent.service';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { User } from '../decorators/user.decorator';
interface ChatRequest {
messages: Array<{
role: 'user' | 'assistant';
content: string;
}>;
}
interface UserPayload {
sub: string;
email: string;
role: string;
}
@Controller('agents')
@UseGuards(JwtAuthGuard)
export class AgentsController {
private readonly logger = new Logger(AgentsController.name);
constructor(private readonly agentService: AgentService) {}
@Post(':sessionId')
async streamAgent(
@Param('sessionId') sessionId: string,
@Body() body: ChatRequest,
@Req() req: Request,
@Res() res: Response,
@User() user: UserPayload,
) {
try {
// Convert Express request to Web API Request
const webRequest = new Request(`${req.protocol}://${req.get('host')}${req.originalUrl}`, {
method: req.method,
headers: req.headers as any,
body: JSON.stringify(body),
});
const response = await this.agentService.handleAgentRequest(
sessionId,
webRequest,
user.sub,
{
userRole: user.role,
controller: 'AgentsController',
action: 'streamAgent'
}
);
// Stream response through NestJS/Express
res.status(response.status);
response.headers.forEach((value, key) => {
res.setHeader(key, value);
});
if (response.body) {
const reader = response.body.getReader();
const pump = async () => {
try {
const { done, value } = await reader.read();
if (done) return res.end();
res.write(value);
pump();
} catch (error) {
this.logger.error('Streaming error:', error);
res.end();
}
};
pump();
} else {
res.end();
}
} catch (error) {
this.logger.error(`Error in streamAgent for session ${sessionId}:`, error);
throw new HttpException(
'Internal Server Error',
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
@Get(':sessionId/history')
async getSessionHistory(
@Param('sessionId') sessionId: string,
@User() user: UserPayload,
) {
try {
// Verify session ownership
const session = await memory.getSession(sessionId);
if (!session || session.resourceId !== user.sub) {
throw new HttpException('Session not found', HttpStatus.NOT_FOUND);
}
const messages = await this.agentService.getSessionHistory(sessionId);
return { messages };
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
this.logger.error(`Error retrieving history for session ${sessionId}:`, error);
throw new HttpException(
'Failed to retrieve session history',
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
@Delete(':sessionId')
async deleteSession(
@Param('sessionId') sessionId: string,
@User() user: UserPayload,
@Res() res: Response,
) {
try {
// Verify session ownership
const session = await memory.getSession(sessionId);
if (!session || session.resourceId !== user.sub) {
throw new HttpException('Session not found', HttpStatus.NOT_FOUND);
}
await this.agentService.deleteSession(sessionId);
res.status(HttpStatus.NO_CONTENT).send();
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
this.logger.error(`Error deleting session ${sessionId}:`, error);
throw new HttpException(
'Failed to delete session',
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
}
Advanced Integration with Guards and Decorators
JWT Authentication Guard
// guards/jwt-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('Missing authorization token');
}
try {
const payload = this.jwtService.verify(token);
request['user'] = payload;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
User Decorator
// decorators/user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
Rate Limiting
// app.module.ts
import { Module } from '@nestjs/common';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
import { AgentsModule } from './modules/agents.module';
@Module({
imports: [
ThrottlerModule.forRoot([{
name: 'short',
ttl: 60000, // 1 minute
limit: 30, // 30 requests per minute
}, {
name: 'medium',
ttl: 600000, // 10 minutes
limit: 100, // 100 requests per 10 minutes
}]),
AgentsModule,
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
Module Organization
Agents Module
// modules/agents.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AgentsController } from '../controllers/agents.controller';
import { AgentService } from '../services/agent.service';
@Module({
imports: [
JwtModule.register({
secret: process.env.JWT_SECRET || 'supersecret',
signOptions: { expiresIn: '24h' },
}),
],
controllers: [AgentsController],
providers: [AgentService],
})
export class AgentsModule {}
Main Application
// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, Logger } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const logger = new Logger('Bootstrap');
// Global validation pipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
// CORS configuration
app.enableCors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
});
// Global prefix
app.setGlobalPrefix('api');
const port = process.env.PORT || 8080;
await app.listen(port);
logger.log(`Application is running on: http://localhost:${port}`);
}
bootstrap();
DTOs and Validation
// dto/chat.dto.ts
import { IsArray, IsString, IsIn, ArrayMinSize, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
class MessageDto {
@IsString()
@IsIn(['user', 'assistant'])
role: 'user' | 'assistant';
@IsString()
content: string;
}
export class ChatDto {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => MessageDto)
messages: MessageDto[];
}
// Updated controller with validation
@Post(':sessionId')
async streamAgent(
@Param('sessionId') sessionId: string,
@Body() chatDto: ChatDto, // Now validated automatically
@Req() req: Request,
@Res() res: Response,
@User() user: UserPayload,
) {
// Implementation...
}
Interceptors and Middleware
Logging Interceptor
// interceptors/logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const method = request.method;
const url = request.url;
const now = Date.now();
return next
.handle()
.pipe(
tap(() => {
const response = context.switchToHttp().getResponse();
const delay = Date.now() - now;
this.logger.log(`${method} ${url} ${response.statusCode} - ${delay}ms`);
}),
);
}
}
Request Context Middleware
// middleware/request-context.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const requestId = uuidv4();
req['requestId'] = requestId;
res.setHeader('X-Request-ID', requestId);
next();
}
}
Testing
Unit Tests
// controllers/agents.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AgentsController } from './agents.controller';
import { AgentService } from '../services/agent.service';
import { JwtService } from '@nestjs/jwt';
describe('AgentsController', () => {
let controller: AgentsController;
let agentService: AgentService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AgentsController],
providers: [
{
provide: AgentService,
useValue: {
handleAgentRequest: jest.fn(),
getSessionHistory: jest.fn(),
deleteSession: jest.fn(),
},
},
{
provide: JwtService,
useValue: {
verify: jest.fn(),
},
},
],
}).compile();
controller = module.get<AgentsController>(AgentsController);
agentService = module.get<AgentService>(AgentService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('streamAgent', () => {
it('should handle chat requests', async () => {
const mockResponse = new Response();
jest.spyOn(agentService, 'handleAgentRequest').mockResolvedValue(mockResponse);
// Test implementation...
});
});
});
Integration Tests
// test/agents.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('AgentsController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/agents/:sessionId (POST)', () => {
return request(app.getHttpServer())
.post('/agents/test-session')
.send({
messages: [
{ role: 'user', content: 'Hello' }
]
})
.expect(401); // Without auth token
});
afterAll(async () => {
await app.close();
});
});
Configuration
Environment Configuration
// config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT, 10) || 8080,
jwt: {
secret: process.env.JWT_SECRET || 'supersecret',
expiresIn: process.env.JWT_EXPIRES_IN || '24h',
},
redis: {
url: process.env.REDIS_URL,
token: process.env.REDIS_TOKEN,
},
openai: {
apiKey: process.env.OPENAI_API_KEY,
},
});
// app.module.ts
import { ConfigModule } from '@nestjs/config';
import configuration from './config/configuration';
@Module({
imports: [
ConfigModule.forRoot({
load: [configuration],
isGlobal: true,
validationSchema: Joi.object({
PORT: Joi.number().default(8080),
JWT_SECRET: Joi.string().required(),
REDIS_URL: Joi.string().required(),
REDIS_TOKEN: Joi.string().required(),
OPENAI_API_KEY: Joi.string().required(),
}),
}),
// Other modules...
],
})
export class AppModule {}
Deployment
Docker
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
EXPOSE 8080
USER node
CMD ["node", "dist/main"]
Health Check
// health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheckService, HealthCheck, MemoryHealthIndicator } from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private memory: MemoryHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024),
() => this.memory.checkRSS('memory_rss', 150 * 1024 * 1024),
]);
}
}
Advantages
- Architecture: Excellent for large-scale applications
- TypeScript: First-class TypeScript support
- Dependency Injection: Powerful DI container
- Decorators: Clean, declarative code with decorators
- Testing: Excellent testing utilities and patterns
- Ecosystem: Rich ecosystem of modules and integrations
Disadvantages
- Learning Curve: Steeper learning curve than simpler frameworks
- Overhead: More complex setup and structure
- Manual Streaming: Requires manual conversion for Web API streaming
- Bundle Size: Larger application footprint
Best Practices
- Use DTOs for request/response validation
- Implement proper guards for authentication and authorization
- Use interceptors for cross-cutting concerns
- Write comprehensive tests using NestJS testing utilities
- Organize code into modules for better maintainability
- Use configuration modules for environment management