GUÍA PARA CONSTRUIR APIS CON NESTJS PARA PRINCIPIANTES
Introducción a NestJS y APIs RESTful
NestJS es un marco MVC para construir aplicaciones del lado del servidor con Node.js, diseñado para ser eficiente y escalable. Utiliza TypeScript de forma nativa, pero permite programar en JavaScript puro, combinando paradigmas como programación orientada a objetos, programación funcional y programación reactiva funcional. Su arquitectura predefinida facilita la creación de aplicaciones testables, escalables y mantenibles, ideal para desarrolladores que buscan estructura en proyectos backend. Este tutorial guía paso a paso en la construcción de una API RESTful de un blog con NestJS, integrando Sequelize para la base de datos Postgres, autenticación con Passport, validación de entradas y protección de rutas con JWT.
Para seguir este tutorial, es necesario tener conocimientos básicos de TypeScript y JavaScript. Experiencia con Angular es útil, pero no imprescindible, ya que se explicarán todos los conceptos necesarios. Se requiere tener instalado Node.js (versión >= 16 recomendada para 2025), Postman para probar endpoints y una base de datos Postgres configurada.
Instalación y configuración inicial
Para comenzar, instala la CLI de NestJS globalmente para facilitar la creación de proyectos. Ejecuta el siguiente comando en tu terminal:
npm i -g @nestjs/cli
Crea un nuevo proyecto y accede al directorio generado:
nest new nest-blog-api
cd nest-blog-api
npm run start:dev
Abre tu navegador en http://localhost:3000 y verifica que aparece el mensaje “Hello World”. Esto confirma que la aplicación NestJS está funcionando correctamente. La estructura inicial del proyecto será:
nest-blog-api/
├── src/
│ ├── app.module.ts
│ ├── main.ts
├── package.json
├── tsconfig.json
Si encuentras errores al ejecutar npm run start:dev, verifica que la versión de TypeScript en package.json sea compatible (por ejemplo, typescript: ^5.2.2 para 2025). Elimina node_modules y package-lock.json, luego ejecuta npm install.
Configuración de Sequelize y Postgres
Instala las dependencias necesarias para integrar Sequelize con Postgres:
npm install -g sequelize
npm install --save sequelize sequelize-typescript pg pg-hstore
npm install --save-dev @types/sequelize
npm install --save dotenv
Crea un módulo de base de datos con el comando:
nest generate module core/database
Interfaz de configuración de la base de datos
Dentro de src/core/database, crea una carpeta interfaces y un archivo dbConfig.interface.ts:
export interface IDatabaseConfigAttributes {
username?: string;
password?: string;
database?: string;
host?: string;
port?: number | string;
dialect?: string;
urlDatabase?: string;
}
export interface IDatabaseConfig {
development: IDatabaseConfigAttributes;
test: IDatabaseConfigAttributes;
production: IDatabaseConfigAttributes;
}
Configuración de entornos
Crea el archivo database.config.ts en src/core/database:
import * as dotenv from "dotenv";
import { IDatabaseConfig } from "./interfaces/dbConfig.interface";
dotenv.config();
export const databaseConfig: IDatabaseConfig = {
development: {
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME_DEVELOPMENT,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: process.env.DB_DIALECT,
},
test: {
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME_TEST,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: process.env.DB_DIALECT,
},
production: {
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME_PRODUCTION,
host: process.env.DB_HOST,
dialect: process.env.DB_DIALECT,
},
};
Archivo de entorno
En la raíz del proyecto, crea los archivos .env y .env.sample:
DB_HOST=localhost
DB_PORT=5432
DB_USER=tu_usuario
DB_PASS=tu_contraseña
DB_DIALECT=postgres
DB_NAME_TEST=test_db
DB_NAME_DEVELOPMENT=dev_db
DB_NAME_PRODUCTION=prod_db
JWTKEY=tu_clave_secreta
TOKEN_EXPIRATION=48h
BEARER=Bearer
Asegúrate de configurar .env con los valores correctos y añadirlo a .gitignore. Instala el paquete @nestjs/config para gestionar variables de entorno:
npm install --save @nestjs/config
Actualiza app.module.ts para cargar las variables de entorno globalmente:
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true })],
})
export class AppModule {}
Proveedor de base de datos
Crea database.providers.ts en src/core/database:
import { Sequelize } from "sequelize-typescript";
import { SEQUELIZE, DEVELOPMENT, TEST, PRODUCTION } from "../constants";
import { databaseConfig } from "./database.config";
export const databaseProviders = [
{
provide: SEQUELIZE,
useFactory: async () => {
let config;
switch (process.env.NODE_ENV) {
case DEVELOPMENT:
config = databaseConfig.development;
break;
case TEST:
config = databaseConfig.test;
break;
case PRODUCTION:
config = databaseConfig.production;
break;
default:
config = databaseConfig.development;
}
const sequelize = new Sequelize(config);
sequelize.addModels([]);
await sequelize.sync();
return sequelize;
},
},
];
Crea una carpeta constants en src/core con un archivo index.ts:
export const SEQUELIZE = "SEQUELIZE";
export const DEVELOPMENT = "development";
export const TEST = "test";
export const PRODUCTION = "production";
Actualiza database.module.ts:
import { Module } from "@nestjs/common";
import { databaseProviders } from "./database.providers";
@Module({
providers: [...databaseProviders],
exports: [...databaseProviders],
})
export class DatabaseModule {}
Importa el módulo en app.module.ts:
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { DatabaseModule } from "./core/database/database.module";
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true }), DatabaseModule],
})
export class AppModule {}
Prefijo global para endpoints
Configura un prefijo global api/v1 en main.ts:
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix("api/v1");
await app.listen(3000);
}
bootstrap();
Módulo de usuarios
Crea el módulo de usuarios para gestionar operaciones relacionadas con usuarios:
nest generate module modules/users
Servicio de usuarios
Genera el servicio:
nest generate service modules/users
Modelo de usuario
Crea user.entity.ts en src/modules/users:
import { Table, Column, Model, DataType } from "sequelize-typescript";
@Table
export class User extends Model<User> {
@Column({
type: DataType.STRING,
allowNull: false,
})
name: string;
@Column({
type: DataType.STRING,
unique: true,
allowNull: false,
})
email: string;
@Column({
type: DataType.STRING,
allowNull: false,
})
password: string;
@Column({
type: DataType.ENUM,
values: ["male", "female"],
allowNull: false,
})
gender: string;
}
DTO de usuario
Crea una carpeta dto en src/modules/users y un archivo user.dto.ts:
export class UserDto {
readonly name: string;
readonly email: string;
readonly password: string;
readonly gender: string;
}
Proveedor de usuarios
Crea users.providers.ts en src/modules/users:
import { User } from "./user.entity";
import { USER_REPOSITORY } from "../../core/constants";
export const usersProviders = [
{
provide: USER_REPOSITORY,
useValue: User,
},
];
Añade la constante USER_REPOSITORY en src/core/constants/index.ts:
export const USER_REPOSITORY = "USER_REPOSITORY";
Actualiza users.module.ts:
import { Module } from "@nestjs/common";
import { UsersService } from "./users.service";
import { usersProviders } from "./users.providers";
@Module({
providers: [UsersService, ...usersProviders],
exports: [UsersService],
})
export class UsersModule {}
Servicio de usuarios
Actualiza users.service.ts:
import { Injectable, Inject } from "@nestjs/common";
import { User } from "./user.entity";
import { UserDto } from "./dto/user.dto";
import { USER_REPOSITORY } from "../../core/constants";
@Injectable()
export class UsersService {
constructor(
@Inject(USER_REPOSITORY) private readonly userRepository: typeof User
) {}
async create(user: UserDto): Promise<User> {
return await this.userRepository.create<User>(user);
}
async findOneByEmail(email: string): Promise<User> {
return await this.userRepository.findOne<User>({ where: { email } });
}
async findOneById(id: number): Promise<User> {
return await this.userRepository.findOne<User>({ where: { id } });
}
}
Añade el modelo User a database.providers.ts:
import { Sequelize } from "sequelize-typescript";
import { SEQUELIZE, DEVELOPMENT, TEST, PRODUCTION } from "../constants";
import { databaseConfig } from "./database.config";
import { User } from "../../modules/users/user.entity";
export const databaseProviders = [
{
provide: SEQUELIZE,
useFactory: async () => {
let config;
switch (process.env.NODE_ENV) {
case DEVELOPMENT:
config = databaseConfig.development;
break;
case TEST:
config = databaseConfig.test;
break;
case PRODUCTION:
config = databaseConfig.production;
break;
default:
config = databaseConfig.development;
}
const sequelize = new Sequelize(config);
sequelize.addModels([User]);
await sequelize.sync();
return sequelize;
},
},
];
Módulo de autenticación
Crea el módulo de autenticación:
nest generate module modules/auth
nest generate service modules/auth
nest generate controller modules/auth
Estrategia local de Passport
Instala las dependencias necesarias:
npm install --save @nestjs/passport passport passport-local
npm install --save-dev @types/passport-local
npm install --save bcrypt
Crea local.strategy.ts en src/modules/auth:
import { Strategy } from "passport-local";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { AuthService } from "./auth.service";
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException("Credenciales inválidas");
}
return user;
}
}
Actualiza auth.module.ts:
import { Module } from "@nestjs/common";
import { PassportModule } from "@nestjs/passport";
import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
import { UsersModule } from "../users/users.module";
import { LocalStrategy } from "./local.strategy";
@Module({
imports: [PassportModule, UsersModule],
providers: [AuthService, LocalStrategy],
controllers: [AuthController],
})
export class AuthModule {}
Servicio de autenticación
Actualiza auth.service.ts:
import { Injectable } from "@nestjs/common";
import * as bcrypt from "bcrypt";
import { UsersService } from "../users/users.service";
@Injectable()
export class AuthService {
constructor(private readonly userService: UsersService) {}
async validateUser(username: string, pass: string) {
const user = await this.userService.findOneByEmail(username);
if (!user) {
return null;
}
const match = await this.comparePassword(pass, user.password);
if (!match) {
return null;
}
const { password, ...result } = user["dataValues"];
return result;
}
private async comparePassword(enteredPassword, dbPassword) {
const match = await bcrypt.compare(enteredPassword, dbPassword);
return match;
}
}
Estrategia JWT
Instala las dependencias para JWT:
npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
Crea jwt.strategy.ts en src/modules/auth:
import { ExtractJwt, Strategy } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { UsersService } from "../users/users.service";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly userService: UsersService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWTKEY,
});
}
async validate(payload: any) {
const user = await this.userService.findOneById(payload.id);
if (!user) {
throw new UnauthorizedException("No autorizado");
}
return payload;
}
}
Actualiza auth.module.ts para incluir JWT:
import { Module } from "@nestjs/common";
import { PassportModule } from "@nestjs/passport";
import { JwtModule } from "@nestjs/jwt";
import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
import { UsersModule } from "../users/users.module";
import { LocalStrategy } from "./local.strategy";
import { JwtStrategy } from "./jwt.strategy";
@Module({
imports: [
PassportModule,
UsersModule,
JwtModule.register({
secret: process.env.JWTKEY,
signOptions: { expiresIn: process.env.TOKEN_EXPIRATION },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
controllers: [AuthController],
})
export class AuthModule {}
Métodos adicionales en AuthService
Actualiza auth.service.ts para incluir métodos de login y signup:
import { Injectable } from "@nestjs/common";
import * as bcrypt from "bcrypt";
import { JwtService } from "@nestjs/jwt";
import { UsersService } from "../users/users.service";
@Injectable()
export class AuthService {
constructor(
private readonly userService: UsersService,
private readonly jwtService: JwtService
) {}
async validateUser(username: string, pass: string) {
const user = await this.userService.findOneByEmail(username);
if (!user) {
return null;
}
const match = await this.comparePassword(pass, user.password);
if (!match) {
return null;
}
const { password, ...result } = user["dataValues"];
return result;
}
public async login(user) {
const token = await this.generateToken(user);
return { user, token };
}
public async create(user) {
const pass = await this.hashPassword(user.password);
const newUser = await this.userService.create({
...user,
password: pass,
});
const { password, ...result } = newUser["dataValues"];
const token = await this.generateToken(result);
return { user: result, token };
}
private async generateToken(user) {
const token = await this.jwtService.signAsync(user);
return token;
}
private async hashPassword(password) {
const hash = await bcrypt.hash(password, 10);
return hash;
}
private async comparePassword(enteredPassword, dbPassword) {
const match = await bcrypt.compare(enteredPassword, dbPassword);
return match;
}
}
Controlador de autenticación
Actualiza auth.controller.ts:
import { Controller, Body, Post, UseGuards, Request } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { AuthService } from "./auth.service";
import { UserDto } from "../users/dto/user.dto";
@Controller("auth")
export class AuthController {
constructor(private authService: AuthService) {}
@UseGuards(AuthGuard("local"))
@Post("login")
async login(@Request() req) {
return await this.authService.login(req.user);
}
@Post("signup")
async signUp(@Body() user: UserDto) {
return await this.authService.create(user);
}
}
Validación de datos
Instala las dependencias para validación:
npm install --save class-validator class-transformer
Crea una carpeta pipes en src/core y un archivo validate.pipe.ts:
import {
Injectable,
ArgumentMetadata,
BadRequestException,
ValidationPipe,
UnprocessableEntityException,
} from "@nestjs/common";
@Injectable()
export class ValidateInputPipe extends ValidationPipe {
public async transform(value, metadata: ArgumentMetadata) {
try {
return await super.transform(value, metadata);
} catch (e) {
if (e instanceof BadRequestException) {
throw new UnprocessableEntityException(
this.handleError(e.message.message)
);
}
}
}
private handleError(errors) {
return errors.map((error) => error.constraints);
}
}
Añade el pipe globalmente en main.ts:
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidateInputPipe } from "./core/pipes/validate.pipe";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix("api/v1");
app.useGlobalPipes(new ValidateInputPipe());
await app.listen(3000);
}
bootstrap();
Actualiza user.dto.ts para incluir validaciones:
import { IsNotEmpty, MinLength, IsEmail, IsEnum } from "class-validator";
enum Gender {
MALE = "male",
FEMALE = "female",
}
export class UserDto {
@IsNotEmpty()
readonly name: string;
@IsNotEmpty()
@IsEmail()
readonly email: string;
@IsNotEmpty()
@MinLength(6)
readonly password: string;
@IsNotEmpty()
@IsEnum(Gender, {
message: "El género debe ser masculino o femenino",
})
readonly gender: Gender;
}
Guardia para usuarios únicos
Crea una carpeta guards en src/core y un archivo doesUserExist.guard.ts:
import {
CanActivate,
ExecutionContext,
Injectable,
ForbiddenException,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { UsersService } from "../../modules/users/users.service";
@Injectable()
export class DoesUserExist implements CanActivate {
constructor(private readonly userService: UsersService) {}
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
async validateRequest(request) {
const userExist = await this.userService.findOneByEmail(
request.body.email
);
if (userExist) {
throw new ForbiddenException("El email ya existe");
}
return true;
}
}
Actualiza auth.controller.ts para usar la guardia:
import { Controller, Body, Post, UseGuards, Request } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { AuthService } from "./auth.service";
import { UserDto } from "../users/dto/user.dto";
import { DoesUserExist } from "../../core/guards/doesUserExist.guard";
@Controller("auth")
export class AuthController {
constructor(private authService: AuthService) {}
@UseGuards(AuthGuard("local"))
@Post("login")
async login(@Request() req) {
return await this.authService.login(req.user);
}
@UseGuards(DoesUserExist)
@Post("signup")
async signUp(@Body() user: UserDto) {
return await this.authService.create(user);
}
}
Módulo de publicaciones
Crea el módulo de publicaciones:
nest generate module modules/posts
nest generate service modules/posts
nest generate controller modules/posts
Entidad de publicación
Crea post.entity.ts en src/modules/posts:
import {
Table,
Column,
Model,
DataType,
ForeignKey,
BelongsTo,
} from "sequelize-typescript";
import { User } from "../users/user.entity";
@Table
export class Post extends Model<Post> {
@Column({
type: DataType.STRING,
allowNull: false,
})
title: string;
@Column({
type: DataType.TEXT,
allowNull: false,
})
body: string;
@ForeignKey(() => User)
@Column({
type: DataType.INTEGER,
allowNull: false,
})
userId: number;
@BelongsTo(() => User)
user: User;
}
DTO de publicación
Crea una carpeta dto en src/modules/posts y un archivo post.dto.ts:
import { IsNotEmpty, MinLength } from "class-validator";
export class PostDto {
@IsNotEmpty()
@MinLength(4)
readonly title: string;
@IsNotEmpty()
readonly body: string;
}
Proveedor de publicaciones
Crea posts.providers.ts en src/modules/posts:
import { Post } from "./post.entity";
import { POST_REPOSITORY } from "../../core/constants";
export const postsProviders = [
{
provide: POST_REPOSITORY,
useValue: Post,
},
];
Añade la constante POST_REPOSITORY en src/core/constants/index.ts:
export const POST_REPOSITORY = "POST_REPOSITORY";
Actualiza posts.module.ts:
import { Module } from "@nestjs/common";
import { PostsService } from "./posts.service";
import { PostsController } from "./posts.controller";
import { postsProviders } from "./posts.providers";
@Module({
divers: [PostsService, ...postsProviders],
controllers: [PostsController],
})
export class PostsModule {}
Añade el modelo Post a database.providers.ts:
import { Sequelize } from "sequelize-typescript";
import { SEQUELIZE, DEVELOPMENT, TEST, PRODUCTION } from "../constants";
import { databaseConfig } from "./database.config";
import { User } from "../../modules/users/user.entity";
import { Post } from "../../modules/posts/post.entity";
export const databaseProviders = [
{
provide: SEQUELIZE,
useFactory: async () => {
let config;
switch (process.env.NODE_ENV) {
case DEVELOPMENT:
config = databaseConfig.development;
break;
case TEST:
config = databaseConfig.test;
break;
case PRODUCTION:
config = databaseConfig.production;
break;
default:
config = databaseConfig.development;
}
const sequelize = new Sequelize(config);
sequelize.addModels([User, Post]);
await sequelize.sync();
return sequelize;
},
},
];
Servicio de publicaciones
Actualiza posts.service.ts:
import { Injectable, Inject } from "@nestjs/common";
import { Post } from "./post.entity";
import { PostDto } from "./dto/post.dto";
import { User } from "../users/user.entity";
import { POST_REPOSITORY } from "../../core/constants";
@Injectable()
export class PostsService {
constructor(
@Inject(POST_REPOSITORY) private readonly postRepository: typeof Post
) {}
async create(post: PostDto, userId): Promise<Post> {
return await this.postRepository.create<Post>({ ...post, userId });
}
async findAll(): Promise<Post[]> {
return await this.postRepository.findAll<Post>({
include: [{ model: User, attributes: { exclude: ["password"] } }],
});
}
async findOne(id): Promise<Post> {
return await this.postRepository.findOne({
where: { id },
include: [{ model: User, attributes: { exclude: ["password"] } }],
});
}
async delete(id, userId) {
return await this.postRepository.destroy({ where: { id, userId } });
}
async update(id, data, userId) {
const [numberOfAffectedRows, [updatedPost]] =
await this.postRepository.update(
{ ...data },
{ where: { id, userId }, returning: true }
);
return { numberOfAffectedRows, updatedPost };
}
}
Controlador de publicaciones
Actualiza posts.controller.ts:
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
NotFoundException,
UseGuards,
Request,
} from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { PostsService } from "./posts.service";
import { Post as PostEntity } from "./post.entity";
import { PostDto } from "./dto/post.dto";
@Controller("posts")
export class PostsController {
constructor(private readonly postService: PostsService) {}
@Get()
async findAll() {
return await this.postService.findAll();
}
@Get(":id")
async findOne(@Param("id") id: number): Promise<PostEntity> {
const post = await this.postService.findOne(id);
if (!post) {
throw new NotFoundException("Publicación no encontrada");
}
return post;
}
@UseGuards(AuthGuard("jwt"))
@Post()
async create(@Body() post: PostDto, @Request() req): Promise<PostEntity> {
return await this.postService.create(post, req.user.id);
}
@UseGuards(AuthGuard("jwt"))
@Put(":id")
async update(
@Param("id") id: number,
@Body() post: PostDto,
@Request() req
): Promise<PostEntity> {
const { numberOfAffectedRows, updatedPost } =
await this.postService.update(id, post, req.user.id);
if (numberOfAffectedRows === 0) {
throw new NotFoundException("Publicación no encontrada");
}
return updatedPost;
}
@UseGuards(AuthGuard("jwt"))
@Delete(":id")
async remove(@Param("id") id: number, @Request() req) {
const deleted = await this.postService.delete(id, req.user.id);
if (deleted === 0) {
throw new NotFoundException("Publicación no encontrada");
}
return "Eliminado exitosamente";
}
}
Conclusiones
NestJS ofrece una estructura robusta para desarrollar aplicaciones backend con Node.js, facilitando la creación de APIs RESTful escalables y mantenibles. Este tutorial cubrió la configuración de una API de blog con autenticación, validación y protección de rutas, utilizando herramientas modernas como Sequelize, Passport y JWT. Al implementar módulos, servicios y controladores, se logra una arquitectura limpia y modular, ideal para proyectos de cualquier escala. Los desarrolladores pueden extender esta base para agregar funcionalidades avanzadas, como caching o WebSockets, aprovechando la flexibilidad de NestJS. Explora la documentación oficial de NestJS para profundizar en sus capacidades y optimizar tus proyectos.