Skip to content
← Back to blog
NestJSAngularSaaSTutorialEspanolTypeScript

Como Crear un SaaS con NestJS y Angular: Tutorial Completo

Firas Sayah·July 18, 2026·10 min read

Te voy a ser honesto: mi primer SaaS fue un desastre. Pase cinco meses construyendo autenticacion, webhooks de Stripe, multi-tenancy y un panel admin antes de escribir una sola linea del producto real. Cuando por fin lance, ya habia quemado la mitad del presupuesto. Hoy quiero ahorrarte ese dolor. En este tutorial vas a aprender exactamente como construir las bases de un SaaS profesional -- y como saltar las partes aburridas.

Desarrollador trabajando en codigo TypeScript

Por que NestJS y Angular son ideales para SaaS

Si eres desarrollador en Espana o Latinoamerica, probablemente ya conoces Angular. Es uno de los frameworks frontend mas populares en el mercado hispanohablante, especialmente en empresas grandes y proyectos gubernamentales. Lo que quizas no sabes es que NestJS, el framework backend de Node.js, comparte exactamente la misma filosofia arquitectonica.

Modulos, inyeccion de dependencias, decoradores, TypeScript de punta a punta. Si dominas Angular, NestJS te resultara familiar desde el primer momento. Para entender mejor esta sinergia, te recomiendo nuestro articulo sobre por que Angular y NestJS son la mejor combinacion para SaaS.

En este tutorial vamos a construir una aplicacion SaaS completa. No un ejemplo de juguete, sino una base real sobre la que puedes construir tu producto.

Que vamos a construir

Nuestra aplicacion SaaS tendra:

  • Registro e inicio de sesion con JWT
  • Separacion de datos por tenant (multi-tenancy)
  • API RESTful con validacion
  • Roles y permisos basicos
  • Estructura lista para agregar pagos

Cloudrix SaaS Starter ships with this pre-configured and tested.

Skip weeks of boilerplate — auth, payments, multi-tenancy, and deployment included out of the box.

Try the live demo →

Paso 1: Crear el proyecto

Empezamos con un workspace Nx que aloja backend y frontend juntos:

npx create-nx-workspace@latest mi-saas --preset=nest
cd mi-saas
nx g @nx/angular:app panel

Esto nos da dos aplicaciones en un mismo repositorio: la API en NestJS y el panel en Angular.

Paso 2: Configurar la base de datos

Instalamos TypeORM y PostgreSQL:

npm install @nestjs/typeorm typeorm pg

Configuramos la conexion en el modulo raiz:

// app.module.ts
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST || 'localhost',
      port: parseInt(process.env.DB_PORT) || 5432,
      username: process.env.DB_USER || 'postgres',
      password: process.env.DB_PASSWORD || 'postgres',
      database: process.env.DB_NAME || 'mi_saas',
      autoLoadEntities: true,
      synchronize: process.env.NODE_ENV !== 'production',
    }),
    UsuariosModule,
    AuthModule,
    TenantsModule,
  ],
})
export class AppModule {}

Pro tip: Nunca uses synchronize: true en produccion. Usa migraciones de TypeORM. He visto bases de datos perder columnas por este error -- incluyendo la mia.

Paso 3: La entidad Usuario

Definimos la entidad de usuario con TypeORM:

// usuarios/entities/usuario.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';

@Entity('usuarios')
export class Usuario {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  email: string;

  @Column()
  nombre: string;

  @Column()
  contrasena: string;

  @Column({ default: 'usuario' })
  rol: string;

  @Column()
  tenantId: string;

  @CreateDateColumn()
  creadoEn: Date;
}

Diseno de base de datos con PostgreSQL

Paso 4: Autenticacion con JWT

El servicio de autenticacion maneja registro y login. Para una guia mas detallada sobre autenticacion JWT y OAuth, consulta nuestro articulo dedicado.

// auth/auth.service.ts
@Injectable()
export class AuthService {
  constructor(
    private readonly usuariosService: UsuariosService,
    private readonly jwtService: JwtService,
  ) {}

  async registrar(dto: RegistrarDto) {
    const existente = await this.usuariosService
      .buscarPorEmail(dto.email);
    if (existente) {
      throw new ConflictException('El email ya esta registrado');
    }

    const contrasenaHash = await bcrypt.hash(dto.contrasena, 12);
    const usuario = await this.usuariosService.crear({
      ...dto,
      contrasena: contrasenaHash,
    });

    const tokens = this.generarTokens(usuario);
    return { usuario: this.sinContrasena(usuario), ...tokens };
  }

  async iniciarSesion(dto: IniciarSesionDto) {
    const usuario = await this.usuariosService
      .buscarPorEmail(dto.email);

    if (!usuario ||
        !await bcrypt.compare(dto.contrasena, usuario.contrasena)) {
      throw new UnauthorizedException('Credenciales invalidas');
    }

    const tokens = this.generarTokens(usuario);
    return { usuario: this.sinContrasena(usuario), ...tokens };
  }

  private generarTokens(usuario: Usuario) {
    const payload = {
      sub: usuario.id,
      email: usuario.email,
      tenantId: usuario.tenantId,
      rol: usuario.rol,
    };

    return {
      accessToken: this.jwtService.sign(payload, {
        expiresIn: '15m',
      }),
      refreshToken: this.jwtService.sign(payload, {
        expiresIn: '7d',
      }),
    };
  }

  private sinContrasena(usuario: Usuario) {
    const { contrasena, ...resultado } = usuario;
    return resultado;
  }
}

El controlador expone los endpoints:

// auth/auth.controller.ts
@Controller('api/auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('registrar')
  registrar(@Body() dto: RegistrarDto) {
    return this.authService.registrar(dto);
  }

  @Post('iniciar-sesion')
  iniciarSesion(@Body() dto: IniciarSesionDto) {
    return this.authService.iniciarSesion(dto);
  }

  @Post('renovar-token')
  @UseGuards(JwtAuthGuard)
  renovarToken(@Req() req) {
    return this.authService.generarTokens(req.user);
  }
}

🚀 Quieres saltarte semanas de configuracion? El Cloudrix SaaS Starter incluye autenticacion JWT completa, multi-tenancy, integracion Stripe, RBAC y despliegue Docker -- todo configurado y probado. Mira la demo en vivo.

Paso 5: Multi-tenancy

Cada organizacion necesita sus propios datos aislados. Usamos un interceptor para inyectar el tenant en cada consulta. Si quieres profundizar, tenemos un articulo completo sobre arquitectura multi-tenant con NestJS.

// tenancy/tenant.interceptor.ts
@Injectable()
export class TenantInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    const usuario = request.user;

    if (usuario?.tenantId) {
      request.tenantId = usuario.tenantId;
    }

    return next.handle();
  }
}

En los servicios, filtramos siempre por tenant:

// proyectos/proyectos.service.ts
@Injectable()
export class ProyectosService {
  constructor(
    @InjectRepository(Proyecto)
    private readonly repo: Repository<Proyecto>,
  ) {}

  async listar(tenantId: string) {
    return this.repo.find({
      where: { tenantId },
      order: { creadoEn: 'DESC' },
    });
  }

  async crear(tenantId: string, dto: CrearProyectoDto) {
    const proyecto = this.repo.create({
      ...dto,
      tenantId,
    });
    return this.repo.save(proyecto);
  }
}

Paso 6: Control de acceso basado en roles

Creamos un decorador y guard para los roles. Para una implementacion mas robusta, revisa nuestra guia de RBAC en NestJS.

// auth/decorators/roles.decorator.ts
export const Roles = (...roles: string[]) =>
  SetMetadata('roles', roles);

// auth/guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const rolesRequeridos = this.reflector.getAllAndOverride<string[]>(
      'roles',
      [context.getHandler(), context.getClass()],
    );

    if (!rolesRequeridos) return true;

    const { user } = context.switchToHttp().getRequest();
    return rolesRequeridos.includes(user.rol);
  }
}

Uso en un controlador:

@Delete(':id')
@Roles('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
eliminar(@Param('id') id: string) {
  return this.proyectosService.eliminar(id);
}

Paso 7: El frontend Angular

En el panel Angular, creamos un servicio de autenticacion usando Angular Signals para el manejo de estado:

// auth/auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
  private readonly http = inject(HttpClient);
  private readonly router = inject(Router);

  readonly usuario = signal<Usuario | null>(null);
  readonly autenticado = computed(() => this.usuario() !== null);

  async iniciarSesion(email: string, contrasena: string) {
    const respuesta = await firstValueFrom(
      this.http.post<AuthResponse>('/api/auth/iniciar-sesion', {
        email,
        contrasena,
      }),
    );

    localStorage.setItem('token', respuesta.accessToken);
    this.usuario.set(respuesta.usuario);
    this.router.navigate(['/panel']);
  }
}

Dashboard moderno de Angular

De tutorial a produccion 💡

Este tutorial cubre los fundamentos, pero un SaaS en produccion necesita mucho mas: emails transaccionales, colas de tareas con BullMQ, logs de auditoria, rate limiting, cumplimiento RGPD, tests automatizados y un sistema de despliegue robusto.

SaaS Starter es un boilerplate de NestJS + Angular que incluye todo esto listo para usar. La version Lite esta disponible gratis en GitHub para que puedas empezar hoy mismo. Si quieres ver todas las funcionalidades en accion, incluyendo multi-tenancy completo, integracion con Stripe y panel de administracion, visita la demo en vivo.

Yo lo uso para mis propios proyectos y me ha ahorrado literalmente meses de desarrollo. No pierdas meses construyendo infraestructura -- dedica tu tiempo a lo que hace unico a tu producto.

Listo para empezar? Prueba la demo | Ver precios -- la version Lite es completamente gratis.

F

Firas Sayah

Senior Software Engineer

Full-stack developer with 5+ years building production SaaS applications with NestJS and Angular.