Proyecto Node.js con TypeScript y Clean Architecture para Gestión de Usuarios
Vamos a implementar un sistema de autenticación desde terminal usando Clean Architecture (Link al repositorio del proyecto). Aquí te presento la estructura del proyecto: Estructura del Proyecto src/ ├── core/ # Capa de dominio │ ├── entities/ # Entidades de negocio │ ├── repositories/ # Interfaces de repositorios │ ├── usecases/ # Casos de uso │ └── interfaces/ # Interfaces adicionales ├── infrastructure/ # Capa de infraestructura │ ├── repositories/ # Implementaciones concretas de repositorios │ ├── cli/ # Interfaz de línea de comandos │ └── security/ # Utilidades de seguridad ├── application/ # Capa de aplicación (coordinadores) └── main.ts # Punto de entrada Implementación paso a paso 1. Configuración inicial Primero, instala las dependencias necesarias: npm init -y npm install typescript ts-node @types/node bcryptjs inquirer chalk figlet uuid @types/uuid @types/bcryptjs @types/inquirer @types/figlet --save-dev npx tsc --init 2. Entidad de Usuario (Capa de Dominio) src/core/entities/user.ts: export interface User { id: string; username: string; passwordHash: string; // Nunca almacenar contraseñas en texto plano createdAt: Date; } 3. Interfaz del Repositorio (Capa de Dominio) src/core/repositories/user.repository.ts: import { User } from "../entities/user"; export interface UserRepository { createUser(user: User): Promise; findByUsername(username: string): Promise; findAllUsers(): Promise; verifyPassword(password: string, hash: string): Promise; } 4. Casos de Uso (Capa de Dominio) src/core/usecases/auth.usecase.ts: import { UserRepository } from "../repositories/user.repository"; import { User } from "../entities/user"; export class AuthUseCase { constructor(private readonly userRepository: UserRepository) {} async register(username: string, password: string): Promise { if (!username || !password) { throw new Error("Username and password are required"); } const existingUser = await this.userRepository.findByUsername(username); if (existingUser) { throw new Error("Username already exists"); } const user: User = { id: this.generateUserId(), username, passwordHash: password, createdAt: new Date(), }; return this.userRepository.createUser(user); } async login(username: string, password: string): Promise { const user = await this.userRepository.findByUsername(username); if (!user) { throw new Error("User not found"); } const isValid = await this.userRepository.verifyPassword(password, user.passwordHash); if (!isValid) { throw new Error("Invalid password"); } return user; } private generateUserId(): string { return "temp-id"; } } 5. Implementación del Repositorio (Infraestructura) src/infrastructure/repositories/user.file.repository.ts: import { UserRepository } from "../../core/repositories/user.repository"; import { User } from "../../core/entities/user"; import * as fs from "fs"; import * as path from "path"; import * as bcrypt from "bcryptjs"; import { v4 as uuidv4 } from "uuid"; const FILE_PATH = path.join(__dirname, "../../../data/users.json"); export class UserFileRepository implements UserRepository { private users: User[] = []; constructor() { this.ensureFileExists(); this.loadUsers(); } private ensureFileExists(): void { if (!fs.existsSync(FILE_PATH)) { fs.writeFileSync(FILE_PATH, "[]", "utf-8"); } } private loadUsers(): void { const data = fs.readFileSync(FILE_PATH, "utf-8"); this.users = JSON.parse(data || "[]"); } private saveUsers(): void { fs.writeFileSync(FILE_PATH, JSON.stringify(this.users, null, 2), "utf-8"); } async createUser(user: User): Promise { const salt = await bcrypt.genSalt(10); const passwordHash = await bcrypt.hash(user.passwordHash, salt); const newUser: User = { ...user, id: uuidv4(), passwordHash, createdAt: new Date(), }; this.users.push(newUser); this.saveUsers(); return newUser; } async findByUsername(username: string): Promise { return this.users.find((u) => u.username === username) || null; } async findAllUsers(): Promise { return [...this.users]; } async verifyPassword(password: string, hash: string): Promise { return bcrypt.compare(password, hash); } } 6. CLI Interface (Infraestructura) src/infrastructure/cli/auth.cli.ts: import inquirer from "inquirer"; import chalk from "chalk"; import figlet from "figlet"; import { AuthUseCase } from "../../core/usecases/auth.usecase"; import { User } from "../../core/entities/user"; export class AuthCLI { private currentUser: User | null = null; constructor(private au

Vamos a implementar un sistema de autenticación desde terminal usando Clean Architecture (Link al repositorio del proyecto).
Aquí te presento la estructura del proyecto:
Estructura del Proyecto
src/
├── core/ # Capa de dominio
│ ├── entities/ # Entidades de negocio
│ ├── repositories/ # Interfaces de repositorios
│ ├── usecases/ # Casos de uso
│ └── interfaces/ # Interfaces adicionales
├── infrastructure/ # Capa de infraestructura
│ ├── repositories/ # Implementaciones concretas de repositorios
│ ├── cli/ # Interfaz de línea de comandos
│ └── security/ # Utilidades de seguridad
├── application/ # Capa de aplicación (coordinadores)
└── main.ts # Punto de entrada
Implementación paso a paso
1. Configuración inicial
Primero, instala las dependencias necesarias:
npm init -y
npm install typescript ts-node @types/node bcryptjs inquirer chalk figlet uuid @types/uuid @types/bcryptjs @types/inquirer @types/figlet --save-dev
npx tsc --init
2. Entidad de Usuario (Capa de Dominio)
src/core/entities/user.ts
:
export interface User {
id: string;
username: string;
passwordHash: string; // Nunca almacenar contraseñas en texto plano
createdAt: Date;
}
3. Interfaz del Repositorio (Capa de Dominio)
src/core/repositories/user.repository.ts
:
import { User } from "../entities/user";
export interface UserRepository {
createUser(user: User): Promise<User>;
findByUsername(username: string): Promise<User | null>;
findAllUsers(): Promise<User[]>;
verifyPassword(password: string, hash: string): Promise<boolean>;
}
4. Casos de Uso (Capa de Dominio)
src/core/usecases/auth.usecase.ts
:
import { UserRepository } from "../repositories/user.repository";
import { User } from "../entities/user";
export class AuthUseCase {
constructor(private readonly userRepository: UserRepository) {}
async register(username: string, password: string): Promise<User> {
if (!username || !password) {
throw new Error("Username and password are required");
}
const existingUser = await this.userRepository.findByUsername(username);
if (existingUser) {
throw new Error("Username already exists");
}
const user: User = {
id: this.generateUserId(),
username,
passwordHash: password,
createdAt: new Date(),
};
return this.userRepository.createUser(user);
}
async login(username: string, password: string): Promise<User> {
const user = await this.userRepository.findByUsername(username);
if (!user) {
throw new Error("User not found");
}
const isValid = await this.userRepository.verifyPassword(password, user.passwordHash);
if (!isValid) {
throw new Error("Invalid password");
}
return user;
}
private generateUserId(): string {
return "temp-id";
}
}
5. Implementación del Repositorio (Infraestructura)
src/infrastructure/repositories/user.file.repository.ts
:
import { UserRepository } from "../../core/repositories/user.repository";
import { User } from "../../core/entities/user";
import * as fs from "fs";
import * as path from "path";
import * as bcrypt from "bcryptjs";
import { v4 as uuidv4 } from "uuid";
const FILE_PATH = path.join(__dirname, "../../../data/users.json");
export class UserFileRepository implements UserRepository {
private users: User[] = [];
constructor() {
this.ensureFileExists();
this.loadUsers();
}
private ensureFileExists(): void {
if (!fs.existsSync(FILE_PATH)) {
fs.writeFileSync(FILE_PATH, "[]", "utf-8");
}
}
private loadUsers(): void {
const data = fs.readFileSync(FILE_PATH, "utf-8");
this.users = JSON.parse(data || "[]");
}
private saveUsers(): void {
fs.writeFileSync(FILE_PATH, JSON.stringify(this.users, null, 2), "utf-8");
}
async createUser(user: User): Promise<User> {
const salt = await bcrypt.genSalt(10);
const passwordHash = await bcrypt.hash(user.passwordHash, salt);
const newUser: User = {
...user,
id: uuidv4(),
passwordHash,
createdAt: new Date(),
};
this.users.push(newUser);
this.saveUsers();
return newUser;
}
async findByUsername(username: string): Promise<User | null> {
return this.users.find((u) => u.username === username) || null;
}
async findAllUsers(): Promise<User[]> {
return [...this.users];
}
async verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
}
6. CLI Interface (Infraestructura)
src/infrastructure/cli/auth.cli.ts
:
import inquirer from "inquirer";
import chalk from "chalk";
import figlet from "figlet";
import { AuthUseCase } from "../../core/usecases/auth.usecase";
import { User } from "../../core/entities/user";
export class AuthCLI {
private currentUser: User | null = null;
constructor(private authUseCase: AuthUseCase) {}
async start() {
console.log(chalk.green(figlet.textSync("Auth System", { horizontalLayout: "full" })));
while (true) {
if (!this.currentUser) {
await this.showMainMenu();
} else {
await this.showUserMenu();
}
}
}
private async showMainMenu() {
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "What do you want to do?",
choices: ["Login", "Register", "Exit"],
},
]);
switch (action) {
case "Login":
await this.handleLogin();
break;
case "Register":
await this.handleRegister();
break;
case "Exit":
process.exit(0);
}
}
private async showUserMenu() {
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: `Welcome ${this.currentUser!.username}, what do you want to do?`,
choices: ["Logout", "Exit"],
},
]);
switch (action) {
case "Logout":
this.currentUser = null;
console.log(chalk.blue("You have been logged out"));
break;
case "Exit":
process.exit(0);
}
}
private async handleLogin() {
const { username, password } = await inquirer.prompt([
{
type: "input",
name: "username",
message: "Enter your username:",
},
{
type: "password",
name: "password",
message: "Enter your password:",
mask: "*",
},
]);
try {
this.currentUser = await this.authUseCase.login(username, password);
console.log(chalk.green("Login successful!"));
} catch (error) {
console.log(chalk.red(`Error: ${(error as Error).message}`));
}
}
private async handleRegister() {
const { username, password } = await inquirer.prompt([
{
type: "input",
name: "username",
message: "Choose a username:",
},
{
type: "password",
name: "password",
message: "Choose a password:",
mask: "*",
},
]);
try {
await this.authUseCase.register(username, password);
console.log(chalk.green("Registration successful! You can now login."));
} catch (error) {
console.log(chalk.red(`Error: ${(error as Error).message}`));
}
}
}
7. Punto de Entrada
src/main.ts
:
import { UserFileRepository } from "./infrastructure/repositories/user.file.repository";
import { AuthUseCase } from "./core/usecases/auth.usecase";
import { AuthCLI } from "./infrastructure/cli/auth.cli";
// Configuración e inyección de dependencias
const userRepository = new UserFileRepository();
const authUseCase = new AuthUseCase(userRepository);
const authCLI = new AuthCLI(authUseCase);
// Iniciar la aplicación
authCLI.start().catch((error) => {
console.error("Application error:", error);
process.exit(1);
});
Características implementadas:
-
Clean Architecture:
- Capa de dominio (core) con entidades, repositorios y casos de uso
- Capa de infraestructura con implementaciones concretas
- Desacoplamiento mediante interfaces
-
Seguridad:
- Encriptación de contraseñas con bcrypt
- Nunca se almacenan contraseñas en texto plano
-
Persistencia:
- Almacenamiento en archivo JSON (data/users.json)
- Fácil migración a base de datos cambiando el repositorio
-
Interfaz de usuario:
- CLI interactiva con inquirer
- Menús para registro, login y logout
Cómo ejecutar el proyecto:
- Instala las dependencias como se indicó al principio
- Crea la estructura de directorios y archivos
- Ejecuta con:
npx ts-node src/main.ts
Posibles mejoras:
- Migrar a base de datos (solo implementar nuevo repositorio)
- Añadir más validaciones (fortaleza de contraseña, etc.)
- Implementar JWT para sesiones más robustas
- Añadir tests unitarios e integración
Este proyecto sigue los principios de Clean Architecture manteniendo las capas bien separadas y las dependencias apuntando hacia el centro (dominio).
[