Implementación de un sistema de inventario con NestJS, Turso (SQLite), JWT y Swagger
Este manual cubre el desarrollo completo de una API RESTful para gestión de inventario utilizando el framework NestJS con Turso (SQLite distribuido) como base de datos.
src/
├── app.module.ts # Módulo raíz
├── main.ts # Punto de entrada
├── auth/ # Autenticación JWT
│ ├── auth.module.ts
│ ├── auth.service.ts
│ ├── auth.controller.ts
│ ├── strategies/ # Estrategias Passport
│ └── guards/ # Guards de autenticación
├── products/ # Módulo de productos
│ ├── products.module.ts
│ ├── products.service.ts
│ ├── products.controller.ts
│ ├── dto/ # Data Transfer Objects
│ └── schemas/ # Esquemas Drizzle
├── database/ # Configuración Turso + Drizzle
├── users/ # Módulo de usuarios
└── shared/ # Utilidades comunes
# Instalar Nest CLI globalmente
npm install -g @nestjs/cli
# Crear nuevo proyecto
nest new inventario-api
cd inventario-api
# Instalar dependencias principales
npm install @nestjs/config @nestjs/jwt @nestjs/passport passport passport-jwt
npm install @nestjs/swagger swagger-ui-express bcrypt dotenv cors
npm install drizzle-orm @libsql/client
npm install drizzle-kit --save-dev
# Dependencias de desarrollo
npm install --save-dev @types/passport-jwt @types/bcrypt
# Generar módulos básicos
nest generate module auth
nest generate service auth
nest generate controller auth
nest generate module products
nest generate service products
nest generate controller products
nest generate module users
nest generate service users
nest generate controller users
# Crear directorios adicionales
mkdir src/database src/dto src/schemas
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as schema from './schema';
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
@Module({
providers: [
{
provide: 'DB_CLIENT',
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const client = createClient({
url: config.get('TURSO_DB_URL'),
authToken: config.get('TURSO_DB_AUTH_TOKEN'),
});
return drizzle(client, { schema });
},
},
],
exports: ['DB_CLIENT'],
})
export class DatabaseModule {}
import { integer, sqliteTable, text, real } from 'drizzle-orm/sqlite-core';
export const products = sqliteTable('products', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
description: text('description'),
price: real('price').notNull(),
stock: integer('stock').default(0),
categoryId: integer('category_id').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const categories = sqliteTable('categories', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull().unique(),
});
export type Product = typeof products.$inferSelect;
export type NewProduct = typeof products.$inferInsert;
# Turso Configuration
TURSO_DB_URL=libsql://your-db.turso.io
TURSO_DB_AUTH_TOKEN=your-auth-token
# JWT Configuration
JWT_SECRET=miSuperSecretoComplejo123
JWT_EXPIRES_IN=30d
# App Configuration
PORT=3000
NODE_ENV=development
import { Module } from '@nestjs/common';
import { ProductsService } from './products.service';
import { ProductsController } from './products.controller';
import { DatabaseModule } from '../database/database.module';
@Module({
imports: [DatabaseModule],
controllers: [ProductsController],
providers: [ProductsService],
})
export class ProductsModule {}
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectDatabase } from '../database/database.decorator';
import { DB } from '../database/types';
import { and, eq, like, gte, lte } from 'drizzle-orm';
import { products } from '../database/schema';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
@Injectable()
export class ProductsService {
constructor(@InjectDatabase() private readonly db: DB) {}
async create(createProductDto: CreateProductDto) {
const [product] = await this.db
.insert(products)
.values({
...createProductDto,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning();
return product;
}
async findAll(query: any) {
const { search, minPrice, maxPrice, categoryId } = query;
const conditions = [];
if (search) {
conditions.push(like(products.name, `%${search}%`));
}
if (minPrice) {
conditions.push(gte(products.price, minPrice));
}
if (maxPrice) {
conditions.push(lte(products.price, maxPrice));
}
if (categoryId) {
conditions.push(eq(products.categoryId, categoryId));
}
return this.db
.select()
.from(products)
.where(conditions.length ? and(...conditions) : undefined);
}
async findOne(id: number) {
const [product] = await this.db
.select()
.from(products)
.where(eq(products.id, id));
if (!product) {
throw new NotFoundException(`Product with ID ${id} not found`);
}
return product;
}
async update(id: number, updateProductDto: UpdateProductDto) {
const [product] = await this.db
.update(products)
.set({
...updateProductDto,
updatedAt: new Date(),
})
.where(eq(products.id, id))
.returning();
if (!product) {
throw new NotFoundException(`Product with ID ${id} not found`);
}
return product;
}
async remove(id: number) {
const [product] = await this.db
.delete(products)
.where(eq(products.id, id))
.returning();
if (!product) {
throw new NotFoundException(`Product with ID ${id} not found`);
}
return product;
}
}
import { Controller, Get, Post, Body, Param, Put, Delete, Query, UseGuards } from '@nestjs/common';
import { ProductsService } from './products.service';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
@ApiTags('products')
@ApiBearerAuth()
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@Post()
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Create a new product' })
@ApiResponse({ status: 201, description: 'Product created' })
create(@Body() createProductDto: CreateProductDto) {
return this.productsService.create(createProductDto);
}
@Get()
@ApiOperation({ summary: 'Get all products' })
@ApiResponse({ status: 200, description: 'List of products' })
findAll(@Query() query: any) {
return this.productsService.findAll(query);
}
@Get(':id')
@ApiOperation({ summary: 'Get product by ID' })
@ApiResponse({ status: 200, description: 'Product found' })
@ApiResponse({ status: 404, description: 'Product not found' })
findOne(@Param('id') id: string) {
return this.productsService.findOne(+id);
}
@Put(':id')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Update product' })
@ApiResponse({ status: 200, description: 'Product updated' })
update(@Param('id') id: string, @Body() updateProductDto: UpdateProductDto) {
return this.productsService.update(+id, updateProductDto);
}
@Delete(':id')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Delete product' })
@ApiResponse({ status: 200, description: 'Product deleted' })
remove(@Param('id') id: string) {
return this.productsService.remove(+id);
}
}
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
import { DatabaseModule } from '../database/database.module';
import { users } from '../database/schema';
import { eq } from 'drizzle-orm';
@Module({
imports: [
DatabaseModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: {
expiresIn: configService.get('JWT_EXPIRES_IN'),
},
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [
AuthService,
JwtStrategy,
{
provide: 'USER_REPOSITORY',
useFactory: (db: DB) => ({
findByEmail: async (email: string) => {
const [user] = await db
.select()
.from(users)
.where(eq(users.email, email));
return user;
},
}),
inject: ['DB_CLIENT'],
},
],
exports: [JwtStrategy, PassportModule],
})
export class AuthModule {}
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('JWT_SECRET'),
});
}
async validate(payload: any) {
return {
userId: payload.sub,
email: payload.email,
role: payload.role
};
}
}
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Configuración de Swagger
const config = new DocumentBuilder()
.setTitle('Inventory API')
.setDescription('API para gestión de inventario con Turso')
.setVersion('1.0')
.addBearerAuth(
{ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
'JWT',
)
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
// Habilitar CORS
app.enableCors();
// Validación global
app.useGlobalPipes(new ValidationPipe());
await app.listen(process.env.PORT || 3000);
}
bootstrap();
Accede a la documentación generada en:
GET http://localhost:3000/api
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { ProductsModule } from './products/products.module';
import { UsersModule } from './users/users.module';
import { DatabaseModule } from './database/database.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
DatabaseModule,
AuthModule,
ProductsModule,
UsersModule,
],
})
export class AppModule {}
NODE_ENV=production
PORT=8080
TURSO_DB_URL=libsql://prod-db.turso.io
TURSO_DB_AUTH_TOKEN=your-prod-auth-token
JWT_SECRET=productionSecretKey!987
JWT_EXPIRES_IN=1h
FROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . .
RUN npm run build
EXPOSE 8080
CMD ["node", "dist/main"]