Repository navigation
Reglas de programacion Backend
Reglas obligatorias para todo el código de backend que se cree, modifique o refactorice. Stack: NestJS + TypeScript + TypeORM.
El formato del código (linter/formateador) se define en la Guía de estilo; este documento define el fondo: qué hace cada parte del código y dónde vive.
Ningún commit se realiza sin pasar antes por el formateador y el linter. Un commit con errores de formato o de lint se considera incompleto.
- Responsabilidad única: cada módulo, Controller, Service, Entity, DTO, Guard o helper contiene solo la lógica que le corresponde.
- Reutilización: no duplicar lógica; lo compartido va en un Service, Guard, helper o función reutilizable.
- Consistencia: mismos nombres, estructura y organización en archivos similares.
-
Imports: ordenados y sin los que no se usen. Los que solo se usan como tipo van con
import type. - Tipado: las funciones y métodos de la lógica de la aplicación definen el tipo de sus parámetros y de su retorno. Siempre se tipan los elementos del dominio.
- Evitar
anycuando se pueda definir el tipo correcto, porque desactiva el chequeo justo donde más se necesita. Solo se usa con razón justificada. - No hace falta tipar variables locales obvias que TypeScript infiere (
const age = 25;).
// Evitar
show(req: any, res: any) { ... }
// Preferir
calculateTotal(price: number, tax: number): number { return price + tax; }
- Cada funcionalidad principal tiene su propio módulo (
users,restaurants,reservations,reviews), que agrupa lo altamente relacionado. -
@Module()describe:controllers,providers(services, repositorios, helpers),importsyexports. -
AppModulesolo conecta y configura los módulos. Sin lógica de negocio.
restaurants/
├── restaurant.module.ts
├── restaurant.controller.ts
├── restaurant.service.ts
├── restaurant.entity.ts
└── dto/
├── create-restaurant.dto.ts
└── update-restaurant.dto.ts
HTTP Request → Controller → Service → Repository → Base de datos
- Controller: recibe la petición, lee parámetros/query/body, llama al Service y devuelve la respuesta. No accede a la BD ni a repositorios y no tiene lógica de negocio.
- Service: lógica de negocio y operaciones de BD. Solo los Services acceden a los repositorios de TypeORM.
- Repository: se comunica con la BD vía TypeORM, siempre desde un Service.
- Los providers se inyectan por constructor. Todo Service lleva
@Injectable()y se registra enprovidersdel módulo. - TypeORM usa Data Mapper: siempre entidad + service/repositorio.
// Controller
constructor(private readonly booksService: BooksService) {}
// Service
constructor(@InjectRepository(Book) private readonly booksRepository: Repository<Book>) {
super(BooksService.name);
}
Ejemplo de Controller completo (solo recibe, delega y devuelve):
@Controller('books')
export class BooksController {
constructor(private readonly booksService: BooksService) {}
@Get()
async findAll(): Promise<Book[]> {
return await this.booksService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<Book> {
return await this.booksService.findOne(Number(id));
}
@Post()
async create(@Body() createBookDto: CreateBookDto): Promise<Book> {
return await this.booksService.create(createBookDto);
}
}
Las rutas representan recursos, no acciones (nunca /getRestaurants). Recurso en plural (@Controller('books')) y prefijo global api en main.ts.
| Método | Ruta | Propósito |
|---|---|---|
| GET | /api/restaurants | Listar |
| GET | /api/restaurants/:id | Obtener uno |
| POST | /api/restaurants | Crear |
| PATCH | /api/restaurants/:id | Actualizar parcialmente |
| PUT | /api/restaurants/:id | Reemplazar completo |
| DELETE | /api/restaurants/:id | Eliminar |
El :id de la URL siempre se recibe con ParseIntPipe y no se convierte a mano con Number(id). NestJS se encarga de convertir automáticamente la URL a tipo number antes de entrar a la función. y, si mandan algo que no es un número, rechaza la petición con error 400. Así el Service siempre recibe un número válido.
Una entidad representa una tabla y solo contiene columnas, llaves primarias/foráneas y relaciones.
- Decoradores base:
@Entity(),@PrimaryGeneratedColumn(),@Column(),@CreateDateColumn(). - Columnas opcionales:
nullable: truey tipostring | null. - Relaciones:
OneToMany,ManyToOne,OneToOne. Siempre conRelation<T>para evitar referencias circulares. - En
ManyToOne:@JoinColumny, si aplica,onDelete: 'CASCADE'.
@Entity()
export class Review {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => Book, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'bookId' })
book: Relation<Book>;
}
- Todo dato del frontend entra por un DTO, ubicado en
dto/dentro de su módulo:POST → CreateXDto,PATCH → UpdateXDto. -
Nunca se usa una entidad (ni la interfaz completa) como DTO. El DTO de creación no incluye el
id, porque todavía no existe. - Un DTO no se reescribe a mano copiando la entidad o interfaz: se deriva con
Omit/Pick(oOmitType/PartialTypede@nestjs/mapped-typessi es clase), para que se mantenga sincronizado si el modelo cambia. -
interfacepara objetos y contratos de dominio;typepara uniones, literales e intersecciones. - El Controller recibe el DTO con
@Body()y lo pasa al Service, que hacerepository.create(dto)y luegosave.
import type { BookInterface } from './interfaces/book.interface';
export type CreateBookDto = Omit<BookInterface, 'id'>;
- Toda operación con la BD es asíncrona:
async/await. -
asynchace que la función devuelva una promesa;awaitespera su resultado. - Tipar con
Promise<T>(Promise<Book[]>,Promise<Book | null>). Métodos comofindOneBypueden devolvernull: validar siempre ese caso.
Usar la excepción HTTP de Nest que represente el error: NotFoundException, BadRequestException, UnauthorizedException.
if (!book) throw new NotFoundException('Book not found');
Los estados repetidos se definen una sola vez con enum o type compartido, no escritos a mano en varios archivos.
export enum ReservationStatus {
PENDING = 'pending',
CONFIRMED = 'confirmed',
COMPLETED = 'completed',
CANCELLED = 'cancelled',
}
Se manejan con Guards y decorators (AuthGuard, RoleGuard, Decorators). Los Controllers no repiten validaciones de usuario o rol. De este modo, la lógica de control de acceso se mantiene separada de las responsabilidades del controlador.
Todo valor sensible o dependiente del entorno va en variables de entorno (archivo .env), nunca escrito en el código. La URL de la API siempre va en un .env.
# backend/.env
PORT=3000
CORS_ORIGIN=http://localhost:5173
SQLITE_PATH=./database.sqlite
JWT_SECRET=...
frontend/.env (Vite exige el prefijo VITE_)
VITE_API_BASE_URL=http://localhost:3000
- El frontend la usa así:
axios.create({ baseURL:${import.meta.env.VITE_API_BASE_URL}/api}). - El
.envva en.gitignore; se mantiene un.env.examplesin valores reales. - Prohibido escribir URLs, puertos o secretos directamente en el código.
Se configura globalmente en main.ts, con los orígenes permitidos tomados del .env (CORS_ORIGIN, separados por coma):
app.enableCors({ origin: process.env.CORS_ORIGIN?.split(',') });
app.setGlobalPrefix('api');
await app.listen(process.env.PORT ?? 3000);
Vue → Servicio frontend → Axios → API REST → Controller → Service (BaseService) → Repositorio TypeORM → BD
Cada capa cumple solo su responsabilidad. En el frontend, Axios se llama desde los Services (no desde los componentes) y las vistas los invocan en onMounted.
Dentro de un archivo se separan las partes con comentarios de título (// Imports, // Constructor, // Métodos), solo las que existan. No se comenta cada línea: eso es ruido, no organización.
-
Una VM en Google Cloud con dos contenedores:
frontendybackend. -
Firewall: permitir HTTP/HTTPS y abrir los puertos
3000(backend) y80(frontend). -
Docker Compose en un solo
docker-compose.yml: el backend publica3000:3000conPORT,SQLITE_PATHyCORS_ORIGIN; el frontend publica80:80, recibeVITE_API_BASE_URLydepends_on: backend. Un volumen (backend-data:/data) guarda la base SQLite para que no se pierda. -
Dockerfile del backend en multi-stage: la etapa
builderinstala todo y compila (npm run build); la imagen final instala solo producción (npm ci --omit=dev) y copiadistconCOPY --from=builder. Por eso hay dosnpm install. -
Dockerfile del frontend en multi-stage:
builder(Node) hace un solonpm installynpm run build; la imagen final esnginx:alpiney solo copiadist, sinnode_modules. Imagen final muy pequeña. - Los valores por entorno (
CORS_ORIGIN,VITE_API_BASE_URL) entran porARG/ENV/Compose, no fijos en el código. -
deploy.shexportaVITE_API_BASE_URLyCORS_ORIGINcon la IP pública de la VM y ejecutadocker compose up -d --build.
Las herramientas de inteligencia artificial pueden utilizarse como apoyo durante el desarrollo. Sin embargo:
- El desarrollador debe comprender el código incorporado al proyecto.
- El código generado deberá adaptarse a las reglas definidas por el equipo en este documento y en la Guía de estilo de programación.
- No se deberá incorporar código generado automáticamente que contradiga la arquitectura y los estándares establecidos en el proyecto.
- El objetivo no es únicamente que la aplicación funcione, sino que mantenga los patrones definidos por el equipo.
- Pasó formateador y linter.
- Sin
anyinjustificado ni imports sin usar. - Rutas como recursos en plural, con el método HTTP correcto.
- Controller sin lógica ni repositorios.
- Datos del frontend por DTOs; relaciones con
Relation<T>; estados enenum. - Auth con Guards; CORS en
main.ts - URL de la API, puerto y secretos en
.env(y.enven.gitignore).