¡Oh no, el blog ha caído en los títulos clickbait! ¡Esto debe de ser el fin! Tranquilo, es solo que tengo una lista de consejos que no dan para un artículo cada uno, así que agruparlos en un único artículo tenía más sentido. ¡Vamos a clickbaitear!
¿Cuántos de estos ya haces?
Marca los que ya practicas. El resto enlaza a su sección.
0 de 10
Tienes muchas mejoras rápidas por delante. Empieza por los que faltan, cada uno se configura en minutos.
#Export por defecto vs export con nombre
JavaScript no se diseñó inicialmente para permitir el concepto de módulos. Todo tu código compartía un único ámbito global. Sí, JavaScript no estaba pensado para usarse a la escala a la que se usa hoy. Curioso, ¿verdad?
Así que, cuando vimos que de verdad necesitábamos separar las cosas en ficheros para que el código fuese más manejable, inventamos el concepto de .
;(function () {
var counter = 0
// counter es privado a esta función. Sí, si usas var es global salvo que uses strict mode
})()Que a su vez usamos para apañar el concepto de módulos:
var counterModule = (function () {
var counter = 0
function increment() {
counter++
}
function get() {
return counter
}
return { increment: increment, get: get }
})()
counterModule.increment()
counterModule.get() // 1Y encima de esto definimos una función para importar y un objeto para exportar, que era... Lo has adivinado: require.
// counter.js
var counter = 0
module.exports = {
increment: function () {
counter++
},
}
// main.js
var counter = require('./counter')
counter.increment()Así que inventamos CommonJS, y Node.js lo adoptó. Te cuento todo esto porque al le llevó un tiempo aprobar una propuesta para soportar módulos nativos de verdad (llegaron con ES2015).
Como había una tonelada de código existente que importabas con el nombre que te daba la gana, la propuesta nativa también tenía que soportar eso. Para eso existe export default. Y estoy totalmente en contra.
Cuando tienes un export por defecto en JavaScript puedes importarlo como te apetezca.
// user-repository.js
export default class UserRepository {}
// en otro sitio
import UserRepository from './user-repository'
import UsersRepo from './user-repository'
import Whatever from './user-repository'Si me he molestado en darle a algo un nombre con significado, ¿por qué puedes importarlo con otro nombre?
Lo peor es que si decido renombrar el symbol con el refactor del IDE, normalmente no encuentra todos los sitios donde renombrarlo (independientemente del IDE). Esto pasa porque con un export por defecto no hay un nombre de símbolo compartido, solo una ruta de fichero, así que el IDE no tiene nada que emparejar entre el export y el import.
¡Y sigue! Si usas exports por defecto para exportar un objeto entero estás haciendo tu más grande, porque rompe el .
Así que en vez de esto:
// utils.js
export default {
formatDate,
formatCurrency,
formatPercentage,
}
// en otro sitio
import utils from './utils'
utils.formatDate(new Date())Haz esto:
// utils.js
export function formatDate() {}
export function formatCurrency() {}
export function formatPercentage() {}
// en otro sitio
import { formatDate } from './utils'
formatDate(new Date())¿Y los imports que no conocen el nombre de antemano, como cuando hago lazy loading de rutas?
const UserPage = React.lazy(() => import('./user-page'))Vale, ese es uno de los pocos sitios donde de verdad creo que está justificado. Aun así creo un export con nombre y un fichero que importa ese export con nombre y lo exporta por defecto
// user-page.lazy.js
export { UserPage as default } from './user-page'#Kebab-case vs camelCase (o PascalCase) en los nombres de fichero
En el mundo React se estableció como convención que los componentes siguiesen la convención PascalCase:
// UserProfileCard.jsx
export function UserProfileCard({ user }) {}Esto reflejaba cómo se usa realmente un componente en el código:
<UserProfileCard user={user} />Un caso de cases
camelCase userProfileCard
PascalCase UserProfileCard
snake_case user_profile_card
kebab-case user-profile-cardComo los proyectos nuevos se alejaban de jQuery y se acercaban a React, quedaba raro tener un proyecto donde los componentes de React estaban en PascalCase y el resto de ficheros en camelCase. Al menos eso es lo que creo que pasó.
Sin embargo, en proyectos con camelCase y PascalCase te puedes encontrar con distintos problemas.
El primero, para mí, es la legibilidad.
Encuentra el error:
Encuentra el error
Uno de estos nombres de fichero tiene una errata. Haz clic en él.
También la consistencia:
src/parsers/HTMLParser.ts
src/parsers/HtmlParser.ts
src/clients/HTTPClient.ts
src/clients/HttpClient.tsAdemás, puedes tener problemas al renombrar ficheros en sistemas de ficheros que no distinguen mayúsculas de minúsculas, como macOS o Windows (¡usa git mv!).
Por eso yo siempre elegiría nombres en kebab case.
src/core/components/tooltip-text/tooltip-text.tsx
src/core/metadata/generate-page-metadata.ts
src/features/talks/domain/talk-locations.ts#Constructores con nombre
Cuando tienes una clase en JavaScript puedes tener un constructor:
class Temperature {
constructor(celsius) {
this.celsius = celsius
}
}Un constructor te permite crear una instancia de una clase:
const temperature = new Temperature(20)Y podemos pasar parámetros por el constructor para configurar cómo se crea esa clase
const freezing = new Temperature(0)
const boiling = new Temperature(100)Pero ¿y si quieres tener una especie de constructor con nombre? Por ejemplo, para crear una Temperature a partir de Fahrenheit.
Quizá puedas crear un parámetro unit y luego tener un if en el constructor:
class Temperature {
constructor(value, unit) {
if (unit === 'fahrenheit') {
this.celsius = ((value - 32) * 5) / 9
} else {
this.celsius = value
}
}
}
const temperature = new Temperature(68, 'fahrenheit')O si te pasas a TypeScript puedes tener sobrecarga de constructores
class Temperature {
constructor(celsius: number)
constructor(value: number, unit: 'celsius' | 'fahrenheit')
constructor(value: number, unit: 'celsius' | 'fahrenheit' = 'celsius') {
this.celsius = unit === 'fahrenheit' ? ((value - 32) * 5) / 9 : value
}
}Pero si quieres darle un nombre, no parece que haya una solución limpia, ¿verdad?
Bueno, podemos tener un método estático que cree la instancia. Y ese método estático puede tener nombre
class Temperature {
constructor(celsius) {
this.celsius = celsius
}
static fromCelsius(celsius) {
return new Temperature(celsius)
}
static fromFahrenheit(fahrenheit) {
return new Temperature(((fahrenheit - 32) * 5) / 9)
}
}
const room = Temperature.fromCelsius(20)
const sameRoom = Temperature.fromFahrenheit(68)Simple pero elegante.
Este patrón podría encajar en el patrón de diseño factorySe abre en una nueva pestaña
#Parámetros con nombre
Algo que se apoya en el patrón anterior es tener parámetros con nombre. Me explico: cuando tienes una función o método con muchos parámetros:
createUser('César', 'Alberca', 'cesar@cesalberca.com', true, false, 'es')Es muy fácil equivocarse en su posición, y cuando lees una es imposible saber a qué se refieren si hay varios.
Una función con un parámetro se llama unaria, con dos binaria y con tres ternaria. ¡Curioso!
¿La solución? ¡Objetos! Convierte los parámetros en un objeto y eso te ayuda con este problema en concreto.
createUser({
name: 'César',
surname: 'Alberca',
email: 'cesar@cesalberca.com',
isAdmin: true,
hasNewsletter: false,
locale: 'es',
})Para mis clases en TypeScript tengo un constructor con private readonly y luego constructores con nombre usando funciones estáticas.
type UserRaw = {
name: string
surname: string
email: string
isAdmin: boolean
}
class User {
private constructor(private readonly raw: UserRaw) {}
static create(raw: Omit<UserRaw, 'isAdmin'>): User {
return new User({ ...raw, isAdmin: false })
}
static createAdmin(raw: Omit<UserRaw, 'isAdmin'>): User {
return new User({ ...raw, isAdmin: true })
}
}
const cesar = User.createAdmin({
name: 'César',
surname: 'Alberca',
email: 'cesar@cesalberca.com',
})#Dependencias fijadas
Yo siempre fijo las dependencias, y tú también deberías. Este pequeño símbolo ^ ha formado parte, de una forma u otra, de las mayores amenazas de seguridad del ecosistema de desde sus inicios.
Las dependencias de npm siguen semverSe abre en una nueva pestaña. Cuando instalas con npm (y con el resto de gestores de paquetes importantes):
npm install reactPor defecto registra la dependencia en tu package.json con ese caret.
{
"dependencies": {
"react": "^19.2.0"
}
}Y sorpresa, sorpresa: aunque veas la versión ^19.2.0 en tu package.json, en realidad podrías haber instalado la versión 19.2.14 o la 19.3.0. Ese caret significa que solo respeta la versión major, no la minor ni la patch.
^ se llama caret. Su hermana ~ (la tilde) solo deja moverse a la versión patch.
¿Qué instalará npm?
Escribe un rango de versión y mira qué versiones publicadas acepta. La resaltada es la que elegiría una instalación limpia.
- 19.2.0
- 19.2.1
- 19.2.14
- 19.3.0
- 19.45.0
- 20.0.0
Cumplen 5 versiones. Una instalación limpia sin lockfile elige la 19.45.0.
¿Y el package-lock.json? Bueno, lo introdujeron para guardar la versión exacta de cada dependencia (y de las dependencias de las dependencias). Ayuda, siempre que nadie lo borre "para arreglar un error raro" o cambie un rango en el package.json.
Básicamente, cuando ejecutas npm install sin lockfile (o con uno desactualizado) actualiza automáticamente la dependencia a la última versión que encaje con el caret. A su vez, esto significa que tus dependencias pueden ser distintas de las de tus compañeros ¡o incluso de las de la integración continua!
La gente suele hacer esto para librarse del engorro de actualizar las dependencias sin pensar que, si una dependencia es hackeada como en este casoSe abre en una nueva pestaña, este otroSe abre en una nueva pestaña o este otroSe abre en una nueva pestaña, el siguiente job o compañero que ejecute npm install podría exponer el sistema al atacante.
npm install --save-exact reactUsa npm ci en integración continua. Instala exactamente lo que dice el lockfile y falla si el package.json y el lockfile no coinciden, en vez de resolver versiones nuevas en silencio.
#Mise
¡Tengo una herramienta para gestionar las versiones de mis herramientas! Distintos proyectos a veces necesitan distintas versiones de Node o Python. Para no volverme loco cambiando de versión uso miseSe abre en una nueva pestaña para gestionarlo. También puedes especificar un fichero en tu proyecto para que el resto del equipo y la CI usen las versiones concretas de esa herramienta:
# mise.toml
[tools]
node = "24.1.0"
python = "3.13"
java = "21"Las versiones pueden ser exactas (24.1.0), aproximadas (24), lts o latest. Cuando haces cd al proyecto, mise activa esas versiones automáticamente.
#Variables de entorno
Las variables de entorno son bastante útiles. De hecho, son muy útiles. ¡Es una pena que los agentes de IA las estén exponiendo todo el rato! Pues bien, ¡se acabó! Uso 1Password y su funcionalidad de EnvironmentsSe abre en una nueva pestaña, que me permite guardar esas variables de forma segura. Cuando algo necesita acceder a ellas puedo darle acceso o no, y no pueden leerlas directamente.
# .env
RESEND_API_KEY=op://Development/cesalberca-web/RESEND_API_KEYop run --env-file=.env -- npm start#Varias configuraciones de Vitest
Los tests unitarios y los de integración tienen necesidades distintas, así que los separo en configuraciones distintas. Una configuración raíz que declara los proyectos:
// vitest.config.mts
export default defineConfig({
test: {
projects: ['./vitest.config.unit.ts', './vitest.config.it.ts'],
},
})Una para los tests unitarios, que corren en Node:
// vitest.config.unit.ts
export default defineConfig({
test: {
name: 'unit',
include: ['**/*.test.unit.ts'],
environment: 'node',
},
})Y otra para los tests de integración, que corren en un navegador de verdad:
// vitest.config.it.ts
export default defineConfig({
test: {
name: 'integration',
include: ['**/*.test.it.tsx'],
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
})Así puedo ejecutar vitest --project unit con cada guardado y dejar los lentos para después.
#Pre-commit y pre-push
Uso lefthookSe abre en una nueva pestaña para ejecutar comprobaciones antes de que el código salga de mi máquina. En pre-commit solo linteo y formateo los ficheros en stage:
# lefthook.yml
pre-commit:
commands:
check:
glob: '*.{js,ts,cjs,mjs,jsx,tsx,json,jsonc}'
run: npx @biomejs/biome check --write {staged_files}
stage_fixed: trueY en pre-push, las comprobaciones más lentas:
pre-push:
commands:
types:
run: npm run type:check
tests:
run: npm run test:unit#.editorconfig
El fichero más humilde del repositorio. Le dice a todos los editores (y a todos los agentes de IA) cómo escribir los ficheros: indentación, finales de línea y espacios al final.
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
[*.md]
max_line_length = off
trim_trailing_whitespace = false#Conclusión
Y estos serían algunos de mis mejores consejos para el desarrollo frontend, ¡espero que te hayan gustado! Si no quieres perderte los próximos artículos, suscríbete a la newsletter aquíSe abre en una nueva pestaña.


