Repository navigation
Reglas de programacion frontend
El objetivo de estas reglas es mantener una estructura de desarrollo consistente, mantenible y fácil de comprender para todos los integrantes del equipo, en el proyecto Vue.js.
Las reglas deberán ser respetadas por cualquier código que se incorpore al proyecto, incluyendo código generado con herramientas de inteligencia artificial. La forma (formato/estilo del código) se define en la Guía de estilo de programación; este documento define el fondo: qué debe hacer o dónde debe vivir cada parte del código.
Regla transversal: ningún commit se realiza sin que el código haya pasado antes por el formateador y el linter definidos en la Guía de estilo de programación (ver ese documento). Un commit con errores de formato o de lint se considera un commit incompleto.
Las funciones y métodos relacionados con la lógica de la aplicación deben definir los tipos de sus parámetros y retornos.
function calculateTotal(price: number, tax: number): number {
return price + tax;
}Siempre se deben tipar los elementos relacionados con el dominio de la aplicación: modelos, entidades, DTOs, contratos de API e interfaces.
export interface BookInterface {
id: number;
title: string;
category: string;
price: number;
stock: number;
}Se debe evitar el uso de any cuando sea posible definir correctamente el tipo de la información. any desactiva el chequeo de tipos en ese punto del código y anula la principal ventaja de usar TypeScript.
// Evitar
static show(req: any, res: any) { ... }
// Preferir
static show(req: Request, res: Response): void { ... }No es necesario declarar explícitamente el tipo de variables locales cuando TypeScript pueda inferirlo con claridad. Esto aplica a variables locales obvias, prototipos rápidos o scripts pequeños; no aplica a elementos del dominio (ver 1.2).
const age = 25;- Se utilizará
interfaceprincipalmente para representar objetos y contratos de dominio. - Se utilizará
typepara construir tipos más flexibles o compuestos (uniones, literales, intersecciones, enums). - Las interfaces deberán almacenarse en
src/interfaces/. - Los DTOs deberán almacenarse en
src/dtos/. - Un DTO no se reescribe a mano copiando la interfaz: se deriva de ella con
Omit/Pick, para que quede sincronizado si la interfaz cambia. - No se debe usar la interfaz de dominio completa como tipo de entrada de un formulario de creación (por ejemplo, el
idtodavía no existe en ese punto).
export interface BookInterface {
id: number;
title: string;
category: string;
}
export type CreateBookDTO = Omit<BookInterface, 'id'>;
type Role = 'admin' | 'user' | 'guest';Cuando una importación se utilice únicamente como tipo (una interfaz o un type, no un valor en tiempo de ejecución), se debe usar la sintaxis import type. Esto deja explícito que ese import no genera código en el bundle final.
import type { BookInterface } from '@/interfaces/BookInterface';Las rutas deberán definirse dentro de src/router/ y manejarse mediante Vue Router.
- Toda ruta debe estar asociada a una vista.
- Las rutas deben representar claramente la página a la cual permiten acceder.
- Se utilizarán parámetros dinámicos cuando sea necesario identificar un recurso específico (por ejemplo,
/books/:id). - La lógica de negocio no deberá escribirse directamente dentro de la definición de las rutas.
- La navegación interna del sitio siempre se realiza con el componente
RouterLink; no se utiliza<a href="">, ya que este último recarga la página completa y rompe el comportamiento de la SPA. - Cuando corresponda, podrán utilizarse funcionalidades adicionales de Vue Router: rutas con nombre, rutas anidadas, parámetros dinámicos, route guards, metadatos (
meta) y lazy loading.
{
path: '/books/:id',
name: 'book-show',
component: () => import('@/views/book/BooksShowView.vue'),
}Las vistas representan las diferentes páginas de la aplicación y se almacenan en src/views/.
- Toda vista deberá desarrollarse como un Single File Component (SFC) utilizando archivos
.vue. - Las vistas deberán utilizar TypeScript.
- Una vista deberá enfocarse principalmente en la presentación e interacción con el usuario.
- La lógica de negocio reutilizable no deberá implementarse directamente en la vista; cuando exista una capa de servicios, la vista debe utilizarla.
- Si un dominio tiene más de una vista asociada, estas se agrupan en un subdirectorio con el nombre de ese dominio.
- Los nombres de los archivos de vista deben usar PascalCase y terminar siempre con el sufijo
View(ejemplo:HomeView.vue,BooksIndexView.vue), para distinguirlos a simple vista de un componente reutilizable.
src/views/
└── book/
├── BooksIndexView.vue
├── BooksShowView.vue
└── BooksCreateView.vue
Los componentes deberán desarrollarse utilizando Single File Components (.vue), con la estructura:
<script setup lang="ts">
// Lógica del componente
</script>
<template>
<!-- Interfaz -->
</template>
<style>
/* Estilos cuando sean necesarios */
</style>- Cada componente deberá tener una responsabilidad claramente definida.
- Se utilizará
<script setup lang="ts">(Composition API). - La lógica reutilizable o de negocio deberá delegarse a la capa correspondiente (servicios, store), no vivir en el componente.
- Se utilizará
refcuando un dato necesite ser reactivo, y se accede/modifica mediante.valuedentro del<script>. - Las
propsy losemitsque reciba o emita un componente deben estar correctamente tipados. - Un componente no deberá acceder directamente a un Store. Si necesita leer o modificar estado compartido, debe hacerlo a través del Service correspondiente (ver sección 7, "Comunicación entre capas").
const count = ref(0);
count.value++;Dentro de un mismo archivo (.vue o .ts) se debe separar visualmente las distintas partes del código mediante comentarios de tipo título, para que cualquier integrante del equipo pueda ubicar rápidamente dónde vive cada cosa. No es obligatorio incluir todas las secciones posibles: solo se agregan los comentarios de las secciones que realmente existan en ese archivo, y no se debe comentar cada línea individual (eso es ruido, no organización).
En un <script setup>, cuando corresponda, se sigue un orden similar a:
<script setup lang="ts">
// Imports
import { computed, ref } from 'vue';
// Props
const props = defineProps<{ readonly?: boolean }>();
// Variables
const title = 'Reservas';
// Variables reactivas
const selectedStatus = ref('all');
// Computed
const filteredItems = computed(() => {
// ...
});
// Selectores
const statusOptions = ['all', 'available', 'unavailable'];
// Métodos
function handleAction(): void {
// ...
}
</script>En el <template>, los bloques funcionales importantes también se separan con comentarios, no cada elemento individual:
<template>
<!-- Header -->
<header>...</header>
<!-- Filtros -->
<section>...</section>
<!-- Tabla de resultados -->
<section>...</section>
</template><!-- No recomendado: comentar cada elemento suelto -->
<!-- Botón -->
<button>Guardar</button>Por convención del equipo, se establece un único flujo permitido para acceder o modificar datos:
Vista / Componente
↓
Service
↓
Store
- Solamente los Services podrán comunicarse directamente con los Stores.
- Las vistas y los componentes no deberán importar ni usar un Store directamente.
- Toda operación relacionada con datos se solicita a través del Service correspondiente; es el Service quien decide si consulta el Store, el LocalStorage o, en el futuro, una API.
// No recomendado: la vista accede directamente al store
import { useBookStore } from '@/stores/bookStore';
const bookStore = useBookStore();
bookStore.removeBook(id);// Recomendado: la vista pasa por el service
import { BookService } from '@/services/BookService';
BookService.removeBook(id);// Dentro del service
import { useBookStore } from '@/stores/bookStore';
export class BookService {
static removeBook(id: number): void {
const bookStore = useBookStore();
bookStore.removeBook(id);
}
}Esta regla mantiene ocultas, para las vistas y componentes, tanto la fuente de datos como el mecanismo de estado utilizado, y evita que la misma lógica de acceso a datos se repita en distintas vistas.
La lógica de negocio deberá separarse de las vistas mediante una capa de servicios, almacenada en src/services/ (por ejemplo, BookService.ts).
- Las vistas no deberán conocer directamente cómo se almacenan u obtienen los datos cuando exista un servicio encargado de esta responsabilidad.
- Las operaciones de búsqueda, creación, modificación o transformación de datos deberán delegarse a los servicios correspondientes.
- Los métodos de los servicios deberán tener sus parámetros y retornos correctamente tipados.
- La lógica reutilizable deberá mantenerse fuera de los componentes y vistas; se evita duplicar lógica entre diferentes vistas.
- Los Services no deberán contener código visual (HTML, clases de estilos, plantillas).
- Cada Service se encargará únicamente de las operaciones del dominio que le corresponde.
export class BookService {
static getBooks(): BookInterface[] { /* ... */ }
static getBookById(id: number): BookInterface | undefined { /* ... */ }
static createBook(book: CreateBookDTO): void { /* ... */ }
}En una vista se deberá preferir:
// Correcto
const book = BookService.getBookById(id);
// Evitar dentro de la vista
const book = books.find((b) => b.id === bookId);Las operaciones CRUD (Create, Read, Update, Delete) de una misma entidad deben quedar centralizadas dentro de su Service correspondiente, con nombres de método consistentes en todos los servicios del proyecto:
BookService
├── getBooks()
├── getBookById()
├── createBook()
├── updateBook()
└── deleteBook()
export class BookService {
static getBooks(): BookInterface[] { /* ... */ }
static getBookById(id: number): BookInterface | undefined { /* ... */ }
static createBook(book: CreateBookDTO): void { /* ... */ }
static updateBook(book: BookInterface): void { /* ... */ }
static deleteBook(id: number): void { /* ... */ }
}Las operaciones adicionales que no sean estrictamente CRUD pero pertenezcan al mismo dominio (por ejemplo, confirmar o cancelar algo) también se mantienen dentro de ese mismo Service, y no se dispersan en otros archivos.
Los stores deberán almacenarse en src/stores/.
- Pinia deberá utilizarse únicamente cuando la información necesite ser compartida entre diferentes componentes o vistas. El estado que únicamente pertenece a un componente deberá mantenerse local.
- Los stores deberán utilizar
defineStore, bajo el enfoque de Setup Store. - El estado reactivo deberá manejarse mediante
ref. - Se deberá evitar duplicar el mismo estado en diferentes partes de la aplicación.
- Los componentes y las vistas no deberán acceder directamente a los stores; deben hacerlo a través del Service correspondiente (ver sección 7, "Comunicación entre capas").
export const useBookStore = defineStore('book', () => {
const books = ref<BookInterface[]>([]);
return { books };
});El registro de Pinia se realiza en main.ts, antes de montar la aplicación:
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount('#app');Las funciones generales que no pertenezcan directamente a la lógica de negocio, y cuya única responsabilidad sea transformar la presentación de un dato, deberán almacenarse en src/utils/.
src/utils/
├── formatCurrency.ts
├── formatDate.ts
└── formatTime.ts
- Los utils deben ser funciones pequeñas y reutilizables.
- No deberán contener estado global.
- No deberán contener lógica de negocio ni realizar operaciones CRUD.
- No deberán acceder a Stores.
- No se deben repetir estas funciones copiadas dentro de distintos componentes o vistas: se importan desde
utils/.
export function formatCurrency(value: number): string {
return `$${value.toLocaleString()}`;
}import { formatCurrency } from '@/utils/formatCurrency';- Toda variable de entorno (URLs de API, llaves, flags de ambiente) se debe definir en un archivo
.env, nunca escrita directamente en el código. - El archivo
.envno se sube al repositorio; se documenta su estructura en un.env.examplesin valores sensibles. - El código accede a las variables de entorno de forma tipada (no repitiendo
import.meta.env.LO_QUE_SEAdisperso por el proyecto), idealmente centralizado en un único archivo de configuración.
# .env.example
VITE_API_URL=
Cada archivo, componente, servicio o módulo debe tener una responsabilidad claramente definida. Se deberá buscar aplicar el principio de Single Responsibility Principle (SRP):
- Las rutas se encargan de la navegación.
- Las vistas se encargan de representar páginas.
- Los componentes representan partes reutilizables de la interfaz.
- Los servicios contienen lógica reutilizable o de negocio.
- Los stores manejan estado compartido.
- Las interfaces y DTOs representan contratos de datos.
No aplica ejemplo de código adicional: esta regla se verifica revisando que cada archivo de las categorías anteriores cumpla únicamente su propia responsabilidad.
Además:
- No se debe dejar código, variables o importaciones sin utilizar.
- No se deben agregar capas, archivos, helpers o abstracciones adicionales que no sean realmente necesarios para resolver el problema actual.
- Los métodos, funciones y variables se nombran en
camelCasede forma consistente en toda la clase o archivo (evitar mezclar estilos, por ejemploMain_Pointjunto ashow). - Los atributos de un objeto o modelo inician con minúscula (
category, noCategory). - Las importaciones se organizan en orden alfabético.
- Si un archivo (rutas, controladores, servicios) acumula demasiadas responsabilidades o crece demasiado, se evalúa dividirlo en archivos más específicos.
// Evitar
import { Book } from '../models/Book.js';
import { Router } from 'express';
// Preferir (orden alfabético)
import { Router } from 'express';
import { Book } from '../models/Book.js';Adicionalmente:
- Los grupos de importaciones de distinto origen (librerías externas, Vue, módulos internos del proyecto, tipos) pueden separarse con una línea en blanco entre grupos para mejorar la lectura, manteniendo el orden alfabético dentro de cada grupo.
- Los nombres que el equipo acuerde para un concepto de UI o de dominio se respetan siempre igual, en todo el proyecto, sin usar sinónimos. Por ejemplo: en este proyecto, todo menú desplegable se llamará siempre selector (en variables, props, nombres de componentes, comentarios y documentación), y nunca se alterna con términos como "dropdown", "combo" o "filtro" para referirse a lo mismo. Si el equipo decide cambiar un nombre acordado, se actualiza en todo el proyecto a la vez, no solo en el archivo donde surgió el cambio.
// Evitar mezclar nombres distintos para el mismo concepto
const statusDropdown = ref('all');
const categoryFilter = ref('all');
// Preferir un único término acordado por el equipo: "selector"
const statusSelector = ref('all');
const categorySelector = ref('all');Antes de realizar un push, cada integrante deberá verificar que:
- El código compila correctamente.
- No existen errores de ESLint ni diferencias pendientes de Prettier.
- No existen variables o importaciones sin utilizar.
- No se utilizó
anyinnecesariamente. - Las funciones y métodos importantes están correctamente tipados.
- Las importaciones están organizadas alfabéticamente.
- Las vistas y componentes no acceden directamente a los Stores.
- Los CRUD están implementados mediante Services.
- Los formatters reutilizables están dentro de
utils/. - Los archivos mantienen las secciones organizadas mediante comentarios cuando corresponda.
- Los nombres de conceptos acordados por el equipo (por ejemplo, "selector") se usaron de forma consistente.
- El código respeta la responsabilidad de cada capa.
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.