Cómo empezar un proyecto web sin morir en el intento


Arrancar un proyecto web desde cero tiene un momento peligroso: los primeros 30 minutos. Es cuando se toman decisiones apresuradas que persiguen el proyecto durante meses. Estructura de carpetas improvisada, variables hardcodeadas por todas partes, commits de “prueba inicial” que nunca se limpian. Deuda técnica por todas partes…

Esta guía va sobre esos primeros pasos. Los que nadie te cuenta porque parecen obvios pero claramente no lo son.


1. Antes de escribir una línea de código: planificar

El mayor error al empezar es abrir el editor antes de tener claro qué se va a construir. No hace falta un documento de 50 páginas — hace falta un sitio donde volcar las ideas y organizarlas.

Trello (o cualquier tablero Kanban)

Un tablero Trello con estas columnas es suficiente para cualquier proyecto personal o de equipo pequeño:

  • Backlog — todo lo que quieres hacer, sin orden
  • To Do — lo que toca en este sprint o semana
  • Doing — en lo que estás trabajando ahora mismo (máximo 2-3 tarjetas)
  • Done — terminado y funcionando
  • Parking Lot — ideas interesantes que no puedes abordar ahora pero no quieres perder

La columna Parking Lot es la más infravalorada. Evita que buenas ideas contaminen el foco actual. Cuando se te ocurra “sería genial añadir autenticación con Google”, en vez de ponerte a investigar OAuth en mitad de maquetar el home, va al Parking Lot. Vuelves cuando toca.

Cada tarjeta debería tener: descripción clara, criterio de aceptación (“está hecho cuando…”) y etiquetas por tipo (feature, bug, mejora, investigación).


2. Repositorio desde el primer commit

No “cuando tenga algo que enseñar”. Desde el primer archivo.

git init
git add .
git commit -m "init: estructura inicial del proyecto"

Y súbelo a GitHub/GitLab/Bitbucket inmediatamente. Aunque sea privado. Los beneficios son:

  • Historial desde el día uno
  • Posibilidad de volver a cualquier punto
  • Colaboración desde el primer momento si la necesitas
  • El .gitignore correcto desde el inicio (no después de haber subido node_modules por error)

.gitignore desde el principio

node_modules/
dist/
.env
.env.local
.DS_Store
*.log

El archivo .env nunca debe ir al repositorio. Nunca. Ni en proyectos personales. El hábito de no subirlo se forma desde el primer proyecto.


3. Variables de entorno y configuración

Antes de tocar la base de datos, las APIs o cualquier servicio externo, hay que hacer un inventario de todo lo que el proyecto va a necesitar. No es un paso que se pueda dejar para más tarde: si descubres a mitad de una sesión de código que necesitas una API key de Stripe, pierdes el contexto, te dispersas y acabas con credenciales a medias por todas partes.

Paso 0: inventario de servicios antes de programar

Cuando tengas claro qué va a hacer el proyecto, hazte estas preguntas:

Base de datos

  • ¿MongoDB Atlas? → necesitas la MONGO_URI con usuario, contraseña y nombre del cluster
  • ¿PostgreSQL / MySQL? → DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME
  • ¿Supabase o Firebase? → project URL + anon/service key

Autenticación

  • ¿JWT propio? → genera ya un JWT_SECRET largo y aleatorio (openssl rand -base64 64)
  • ¿OAuth (Google, GitHub…)? → OAUTH_CLIENT_ID + OAUTH_CLIENT_SECRET de la consola del proveedor
  • ¿Auth0 / Clerk / Supabase Auth? → domain + API key

Servicios de terceros

  • Pagos (Stripe, PayPal) → STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET
  • Email (Resend, SendGrid, Nodemailer) → RESEND_API_KEY o credenciales SMTP
  • Almacenamiento (Cloudinary, AWS S3) → CLOUDINARY_URL o AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY
  • Mapas, analytics, IA → cada servicio tiene su key

URLs y entorno

  • PUBLIC_SITE_URL para emails transaccionales y redirects OAuth
  • NODE_ENV para distinguir desarrollo de producción

Con este inventario hecho, crea el .env y el .env.example de golpe, antes de escribir una sola línea de lógica. Así no tendrás que parar a mitad para buscar credenciales ni te arriesgas a hardcodear un valor “provisionalmente”.

# Genera un JWT_SECRET seguro desde la terminal
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
# o con openssl:
openssl rand -base64 64

El archivo .env

# .env (NO subir a git)
MONGO_URI=mongodb+srv://usuario:password@cluster.mongodb.net/miapp
JWT_SECRET=una_clave_muy_larga_y_aleatoria
API_KEY_STRIPE=sk_test_...
PUBLIC_SITE_URL=http://localhost:4321

El archivo .env.example

Este sí va al repositorio. Es la plantilla que cualquier desarrollador (o el tú del futuro) necesita para poner el proyecto en marcha:

# .env.example (SÍ subir a git)
MONGO_URI=
JWT_SECRET=
API_KEY_STRIPE=
PUBLIC_SITE_URL=

Cargarlo en el código

En Node.js — necesitas instalar el paquete dotenv:

npm install dotenv
// Al inicio de tu archivo principal (index.js, server.js...)
import 'dotenv/config';

const uri = process.env.MONGO_URI;
const secret = process.env.JWT_SECRET;

Con import 'dotenv/config' ya está. Lee el .env automáticamente y mete todo en process.env.

En Vite / React — no necesitas instalar nada. Vite lee el .env de forma nativa, pero con una regla importante: solo expone al navegador las variables que empiezan por VITE_:

# .env
VITE_API_URL=https://api.miapp.com
VITE_PUBLIC_KEY=pk_live_...
DB_PASSWORD=secreto  # ← esta NO llega al cliente, solo al servidor
// En cualquier componente React
const apiUrl = import.meta.env.VITE_API_URL;
const pubKey = import.meta.env.VITE_PUBLIC_KEY;

En Astro — también nativo, sin instalación:

// Las variables que empiezan por PUBLIC_ son accesibles en el cliente
const apiUrl = import.meta.env.PUBLIC_SITE_URL;
const secret  = import.meta.env.JWT_SECRET; // solo server-side

Centraliza el acceso a las variables en un único archivo de configuración. Si mañana cambia el nombre de una variable, lo cambias en un sitio, no en veinte.

// src/config.ts
export const config = {
  mongoUri:  import.meta.env.MONGO_URI,
  jwtSecret: import.meta.env.JWT_SECRET,
  siteUrl:   import.meta.env.PUBLIC_SITE_URL,
};

4. Estructura de carpetas

No existe la estructura perfecta universal, pero sí existen estructuras que se pagan más tarde. La regla es: cualquier persona del equipo (o tú en 6 meses) debería encontrar cualquier archivo en menos de 30 segundos.

Una estructura sólida para un proyecto web:

src/
  components/     # componentes reutilizables
  layouts/        # plantillas de página
  pages/          # rutas / vistas
  styles/         # CSS global, variables, reset
  utils/          # funciones auxiliares puras
  lib/            # integraciones externas (db, auth...)
  types/          # tipos TypeScript compartidos
  content/        # contenido estático o colecciones MDX
public/           # assets estáticos (imágenes, fuentes, favicon)
.env
.env.example
.gitignore

Agrupa por tipo de responsabilidad, no por tecnología. Un componente de un formulario de contacto tiene su HTML, CSS y lógica juntos — no el HTML en /templates, el CSS en /styles/forms y el JS en /scripts/forms.


5. Reset CSS: el punto de partida limpio

Cada navegador tiene sus propios estilos por defecto. Los botones, formularios, listas y márgenes se ven diferente en Chrome, Firefox y Safari. El reset CSS borra esas diferencias para empezar desde cero.

Un reset moderno y minimalista que funciona bien:

/* styles/reset.css */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
}

body {
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

input, button, textarea, select {
  font: inherit;
}

p, h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

ul, ol {
  list-style: none;
}

a {
  text-decoration: none;
  color: inherit;
}

Lo importante: se importa antes que cualquier otro CSS. Todo lo que viene después construye sobre este estado limpio predecible.


6. Variables CSS: el sistema de diseño mínimo

Las variables CSS (custom properties) son el lugar donde vive el “lenguaje visual” del proyecto: colores, tamaños, radios, sombras. Definirlas al principio ahorra horas de buscar y reemplazar después.

/* styles/variables.css */
:root {
  /* Colores */
  --color-primary:    #3d5a80;
  --color-secondary:  #e0fbfc;
  --color-accent:     #ee6c4d;
  --color-text:       #1a1a2e;
  --color-text-muted: #6b7280;
  --color-bg:         #ffffff;
  --color-bg-alt:     #f8f9fa;
  --color-border:     #e5e7eb;

  /* Tipografía */
  --font-sans:  'Inter', system-ui, sans-serif;
  --font-mono:  'Fira Code', 'Courier New', monospace;
  --text-xs:    0.75rem;
  --text-sm:    0.875rem;
  --text-base:  1rem;
  --text-lg:    1.125rem;
  --text-xl:    1.25rem;
  --text-2xl:   1.5rem;
  --text-3xl:   1.875rem;
  --text-4xl:   2.25rem;

  /* Espaciado */
  --space-1:  0.25rem;
  --space-2:  0.5rem;
  --space-3:  0.75rem;
  --space-4:  1rem;
  --space-6:  1.5rem;
  --space-8:  2rem;
  --space-12: 3rem;
  --space-16: 4rem;

  /* Layout */
  --content-width: 800px;
  --radius-sm:     4px;
  --radius-md:     8px;
  --radius-lg:     16px;

  /* Sombras */
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.12);
  --shadow-md: 0 4px 12px rgba(0,0,0,0.15);
}

Cuando el cliente pide “cambiar el azul por verde”, cambias una línea. Sin variables, buscas 47 instancias de #3d5a80 por todo el proyecto.


7. Tipografías: Google Fonts sin repetirte

La forma correcta de usar Google Fonts no es copiar el <link> en cada HTML que crees. Es importarlo una sola vez en el CSS global.

Opción 1: desde el CSS global (recomendada)

/* Al inicio de styles/global.css */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap');

Una sola línea. Se carga en toda la web. Si cambias de fuente, lo cambias en un sitio.

Si tu framework usa un layout o plantilla base que se aplica a todas las páginas, ponlo ahí:

<!-- layouts/BaseLayout.astro o _document.tsx, etc. -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet" />

El preconnect le dice al navegador que empiece a conectar con los servidores de Google Fonts antes de que se evalúe el <link> de la fuente — mejora el tiempo de carga.

Después: úsala desde la variable

body {
  font-family: var(--font-sans); /* no hardcodear el nombre aquí */
}

8. El CSS global: el orden importa

Cuando tengas varios archivos CSS, el orden de importación define qué puede sobreescribir qué:

/* styles/global.css — el único que importas en tu layout */

/* 1. Fuentes externas */
@import url('https://fonts.googleapis.com/...');

/* 2. Variables */
@import './variables.css';

/* 3. Reset */
@import './reset.css';

/* 4. Estilos base */
@import './base.css';

/* 5. Utilidades */
@import './utilities.css';

Reset antes que base. Variables antes que todo. Las utilidades al final para poder sobreescribir cuando sea necesario.


9. Preparación del entorno de desarrollo

Node.js y el gestor de paquetes

Fija la versión de Node.js en un archivo .nvmrc o engines en package.json:

{
  "engines": {
    "node": ">=20.0.0"
  }
}

Ahorra el clásico “en mi máquina funciona” cuando un compañero tiene Node 16 y tú tienes Node 22.

Linting y formato

Configúralos el día uno, no cuando el código ya sea un desastre:

npm install -D eslint prettier

Un .prettierrc mínimo:

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5"
}

Con esto, todo el equipo (o el tú del futuro) escribe con el mismo estilo. Sin debates sobre tabs vs espacios.

El README

Escríbelo desde el primer día. No hace falta que sea un manual — solo lo imprescindible:

## Requisitos
- Node.js 20+
- Variables de entorno (ver .env.example)

## Instalación
npm install

## Desarrollo
npm run dev

## Build
npm run build

10. Subida a producción

Un proyecto que solo vive en localhost no es un proyecto, es un borrador. Conectar el repositorio a una plataforma de despliegue cuesta menos de 5 minutos y, a partir de ahí, cada git push actualiza la web automáticamente.

Las opciones más habituales

PlataformaIdeal paraPlan gratuito
VercelNext.js, React, cualquier frontend
NetlifyAstro, sitios estáticos, JAMstack
Cloudflare PagesSitios estáticos, máximo rendimiento global
RenderNode.js, APIs, backends con base de datosSí (con limitaciones)

El flujo básico (igual en todas)

  1. Sube el repositorio a GitHub / GitLab
  2. Crea una cuenta en la plataforma y conecta el repositorio
  3. Configura el comando de build y la carpeta de salida
  4. Añade las variables de entorno en el panel (las del .env)
  5. Despliega — la URL está lista en segundos

Configuración típica

Astro:

Build command: npm run build
Output directory: dist

Next.js en Vercel: Sin configuración. Vercel detecta Next.js automáticamente y lo configura solo.

Node.js / Express en Render:

Build command: npm install
Start command: node index.js

Variables de entorno en producción

Este es el paso que más gente olvida. El .env no está en el repositorio, así que la plataforma no lo ve. Hay que añadir cada variable manualmente en el panel de configuración:

  • Vercel: Settings → Environment Variables
  • Netlify: Site configuration → Environment variables
  • Cloudflare Pages: Settings → Environment variables
  • Render: Environment → Environment Variables

Si una variable falta, el build puede pasar pero la app falla en producción con errores crípticos. El .env.example es la lista de comprobación — cópialo y rellena los valores reales.

Dominios personalizados

Todas estas plataformas asignan una URL por defecto (tu-proyecto.vercel.app, tu-proyecto.netlify.app…). Para usar tu propio dominio:

  1. Añade el dominio en la plataforma
  2. En tu proveedor de DNS, apunta el registro CNAME o A a la dirección que te indica la plataforma
  3. El certificado SSL/TLS se genera automáticamente (HTTPS gratis)

El checklist del día uno

Antes de ponerte a programar de verdad, estas son las casillas que deberían estar marcadas:

  • Tablero Kanban creado con Backlog, To Do, Doing, Done y Parking Lot
  • Repositorio inicializado y subido a remoto
  • .gitignore correcto (node_modules, dist, .env)
  • Inventario de servicios hecho (BBDD, auth, APIs, email, storage…)
  • Credenciales obtenidas y .env + .env.example creados
  • Estructura de carpetas decidida
  • Reset CSS importado
  • Variables CSS con colores, tipografía y espaciado base
  • Tipografías importadas en el CSS global o layout base
  • Linting y formato configurados
  • README con instrucciones de instalación
  • Repositorio conectado a una plataforma de despliegue
  • Variables de entorno configuradas en producción

Esto nos llevará una hora como mucho. A cambio: un proyecto que escala bien, que cualquiera puede clonar y arrancar, y en el que tú mismo no te perderás cuando vuelvas tras dos semanas sin tocarlo.

La preparación no retrasa el proyecto — lo acelera.