LoginSign up
GitHub

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

  1. Use DTOs for request/response validation
  2. Implement proper guards for authentication and authorization
  3. Use interceptors for cross-cutting concerns
  4. Write comprehensive tests using NestJS testing utilities
  5. Organize code into modules for better maintainability
  6. Use configuration modules for environment management