Cambiar tema

Next.js 15 en la práctica: cómo monté un blog de nivel producción en un fin de semana

Easton editorial illustration: API gateway workstation

Introducción

Next.js 15 salió hace poco. Leí la documentación oficial varias veces y vi bastantes tutoriales en vídeo, pero seguía sintiendo que era teoría: Server Actions, App Router… los conceptos los tenía, pero no sabía cómo aplicarlos en un proyecto real. Un fin de semana decidí dejar los tutoriales y hacer algo de verdad: un blog full stack. Frontend, backend, base de datos y despliegue. Dos días después el blog estaba en Vercel con 96 puntos en Lighthouse.

En este artículo comparto todo el proceso. No es un tutorial de copiar y pegar, sino entender el porqué y los agujeros por los que caí. El stack es Next.js 15 + Server Actions + Prisma + PostgreSQL, con código de nivel producción.

¿Por qué Next.js 15?

Elegir el stack me costó bastante.
Cuando salió Next.js 15, en la comunidad había quien decía «otra actualización más, no doy abasto». Siendo honesto, yo también tenía cierta resistencia: la 14 aún no la dominaba del todo. Pero al mirar las novedades con calma, esta actualización sí mereció la pena.

Server Actions: adiós a la pesadez de API Routes

Antes, lo que más me fastidiaba del full stack era escribir API Routes. Crear un api/posts/route.ts, definir POST, parsear el body, devolver la respuesta… siempre el mismo ritual, hasta el cansancio.
Server Actions cambió las reglas. Solo añades 'use server' delante de la función y la llamas desde el componente. La primera vez pensé: «¿Esto es meter código de backend en el frontend?» Lo probé una vez y… vaya diferencia.
Por ejemplo, antes para crear un post hacía algo así:

// Método antiguo: hace falta una API Route
// app/api/posts/route.ts
export async function POST(request: Request) {
  const body = await request.json()
  // montón de lógica...
}
// En el frontend además hay que hacer fetch
const response = await fetch('/api/posts', {
  method: 'POST',
  body: JSON.stringify(data)
})

Ahora basta con esto:

// app/actions/post-actions.ts
'use server'
export async function createPost(formData: FormData) {
  const title = formData.get('title')
  const content = formData.get('content')
  // Acceso directo a la base de datos
  return await prisma.post.create({
    data: { title, content }
  })
}

En el componente es como llamar a una función local. Una analogía: antes ibas tú al restaurante (API Routes); ahora el pedido llega a casa (Server Actions).

Turbopack: desarrollo mucho más rápido

Next.js 15 pasó Turbopack de experimental a estable. Oficialmente: arranque del servidor local un 76,7 % más rápido y actualización de código un 96,3 % más rápida. Al principio pensé que eran cifras de marketing; al usarlo vi que no exageraban.

76.7%
Mejora en arranque del servidor
Datos oficiales de prueba
96.3%
Mejora en actualización de código
Hot reload casi instantáneo
2 s
Tiempo de arranque del proyecto
Más de 30 componentes: de 7-8 s a menos de 2

Mi proyecto tiene más de 30 componentes. Con Webpack tardaba 7-8 segundos en arrancar; con Turbopack queda por debajo de 2. Al cambiar código, el hot reload es casi instantáneo. La mejora en la experiencia de desarrollo es enorme.

¿Por qué este stack encaja con un blog?

Elegí Next.js por tres razones:
Ventaja natural de SSR/SSG — Lo más importante en un blog es el SEO. El render en servidor y la generación estática de Next.js están hechos para eso. El crawler de Google recibe HTML completo; mucho mejor que un React solo en cliente.
Tipado seguro con Prisma — Antes usé Mongoose con MongoDB y los tipos los llevaba a mano; un descuido y aparecían bugs. Prisma genera tipos TypeScript desde el schema; al escribir tienes autocompletado y casi no fallas.
Server Actions simplifican el desarrollo — Sin API Routes, al menos un 30 % menos de código. Con el enfoque clásico habría varios route.ts más.

Elección del stack y diseño de arquitectura

Con el porqué claro, toca el cómo.

Mi stack completo

  • Frontend: Next.js 15 + TypeScript + Tailwind CSS
  • Backend: Next.js Server Actions + NextAuth.js
  • Base de datos: PostgreSQL + Prisma ORM
  • Despliegue: Vercel
    ¿Por qué no MongoDB? Al principio también lo pensé; MongoDB es flexible. Pero Prisma encaja mejor con PostgreSQL, y para posts, usuarios y comentarios —relaciones claras— una base relacional suele ir mejor.

Estructura de directorios

my-blog/
├── app/
│   ├── (auth)/          # Páginas de autenticación
│   ├── blog/            # Páginas del blog
│   │   └── [slug]/      # Ruta dinámica
│   ├── dashboard/       # Panel de usuario
│   ├── actions/         # Server Actions centralizadas
│   └── api/auth/        # Configuración de NextAuth
├── components/          # Componentes reutilizables
├── lib/
│   ├── prisma.ts        # Singleton de Prisma (¡importante!)
│   └── utils.ts         # Utilidades
├── prisma/
│   └── schema.prisma    # Schema de la base de datos
└── public/              # Recursos estáticos

La estructura la fui ajustando varias veces. Al principio tenía Server Actions repartidas en las páginas; luego creé la carpeta actions y todo quedó más ordenado.

Diseño del schema de base de datos

Aquí me metí en un buen lío. La primera versión era demasiado compleja: etiquetas, categorías, estadísticas de lectura… a mitad de camino vi que no necesitaba tantas tablas y perdí medio día simplificando.
El schema final quedó así:

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  image     String?
  posts     Post[]
  createdAt DateTime @default(now())
}
model Post {
  id          String   @id @default(cuid())
  title       String
  slug        String   @unique
  content     String   @db.Text
  published   Boolean  @default(false)
  authorId    String
  author      User     @relation(fields: [authorId], references: [id])
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  @@index([slug])
  @@index([authorId])
}

Fíjate en los dos @@index: clave para el rendimiento. La lista del blog consulta mucho por slug; el perfil del usuario filtra por authorId. Con índices, las consultas van varias veces más rápido.

Implementación de las funciones principales

Arquitectura lista; ahora lo mejor: escribir código.

3.1 Configuración del entorno e inicialización

Crear un proyecto Next.js 15 es muy sencillo:

npx create-next-app@latest my-blog
cd my-blog
npm install prisma @prisma/client zod next-auth

Tras instalar Prisma, inicializa:

npx prisma init

Genera prisma/schema.prisma y .env. En .env configura PostgreSQL:

DATABASE_URL="postgresql://username:password@localhost:5432/myblog?schema=public"

En local uso Docker para PostgreSQL, una línea:

docker run --name blog-postgres -e POSTGRES_PASSWORD=mypassword -p 5432:5432 -d postgres

3.2 Integración de Prisma

Con el schema listo, ejecuta la migración:

npx prisma migrate dev --name init

Cuidado con un detalle: el singleton de Prisma Client. La primera vez que desplegué en Vercel no lo configuré; a las pocas horas apareció Too many connections y pensé que era un DDoS.
En desarrollo, Next.js recarga en caliente y crea una instancia nueva en cada recarga; el pool se agota. La solución:

// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

Parece enrevesado, pero garantiza una sola instancia en desarrollo sin tocar producción. Es la práctica recomendada por Prisma.

3.3 Server Actions en la práctica

Crear posts es el núcleo; así quedan las Server Actions:

// app/actions/post-actions.ts
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
// Validación con Zod para máximo tipado seguro
const PostSchema = z.object({
  title: z.string().min(1, '标题不能为空').max(100),
  content: z.string().min(10, '内容至少10个字'),
  slug: z.string().regex(/^[a-z0-9-]+$/, 'slug只能包含小写字母、数字和横杠')
})
export async function createPost(formData: FormData) {
  // Validar datos
  const validatedFields = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
    slug: formData.get('slug')
  })
  if (!validatedFields.success) {
    return { error: '数据验证失败' }
  }
  try {
    const post = await prisma.post.create({
      data: {
        ...validatedFields.data,
        authorId: 'user-id-here' // En producción, desde la sesión
      }
    })
    // Revalidar caché para que el nuevo post aparezca al instante
    revalidatePath('/blog')
    return { success: true, post }
  } catch (error) {
    return { error: '创建文章失败' }
  }
}

En el componente:

// app/dashboard/new-post/page.tsx
import { createPost } from '@/app/actions/post-actions'
export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="标题" />
      <textarea name="content" placeholder="内容" />
      <input name="slug" placeholder="URL slug" />
      <button type="submit">发布</button>
    </form>
  )
}

¿Lo ves? Sin useState, sin fetch, sin onSubmit. El formulario llama directamente a la Server Action; Next.js se encarga de serialización, red y errores.
Al principio no distinguía Server Actions de API Routes; luego lo entendí: pedido a domicilio frente a ir al restaurante. En la mayoría de casos, Server Actions bastan.

3.4 Autenticación de usuarios

Para auth usé NextAuth.js; la configuración es sencilla:

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GitHubProvider from 'next-auth/providers/github'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'
export const authOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!
    })
  ]
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

Para GitHub OAuth creas una app en GitHub Developer Settings, copias Client ID y Secret a .env. Unos 10 minutos; mucho menos trabajo que un JWT casero.

3.5 Estrategia SSR/SSG

Aquí está la clave del rendimiento. Al principio usé solo SSR y cada visita a la lista del blog pegaba a la base de datos sin necesidad. Pasé a una estrategia mixta:
Lista del blog — SSG + ISR:

// app/blog/page.tsx
import { prisma } from '@/lib/prisma'
// Regenerar cada hora
export const revalidate = 3600
export default async function BlogList() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' }
  })
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content.slice(0, 150)}...</p>
        </article>
      ))}
    </div>
  )
}

La página se genera en el build y se regenera cada hora. El usuario recibe HTML estático; carga rapidísima.
Detalle del post — SSR dinámico:

// app/blog/[slug]/page.tsx
export default async function Post({ params }: { params: { slug: string } }) {
  const post = await prisma.post.findUnique({
    where: { slug: params.slug }
  })
  if (!post) notFound()
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Cada visita consulta la base para el contenido más reciente; ideal para posts que cambian a menudo.
Comparativa de rendimiento:

~200 ms
Tiempo de carga SSG
Generación estática, la más rápida
~800 ms
Tiempo de carga SSR
Render en servidor, ideal para contenido dinámico
~1500 ms
Tiempo de carga CSR puro
Render en cliente, SEO más débil

La diferencia es clara. Por eso Next.js encaja tan bien con blogs: buen SEO y buen rendimiento.

Despliegue y optimización

Código listo; lo más emocionante: publicar.

Despliegue en Vercel

Subí el código a GitHub e importé el repo en Vercel. Detectó Next.js y configuró casi todo. Solo toca las variables de entorno:

DATABASE_URL="tu conexión de base de datos en producción"
GITHUB_ID="OAuth Client ID"
GITHUB_SECRET="OAuth Secret"
NEXTAUTH_URL="https://your-domain.vercel.app"
NEXTAUTH_SECRET="cadena aleatoria"

En producción uso el PostgreSQL gratuito de Supabase: 500 MB al mes, de sobra para un blog personal.
Tras Deploy, en unos 2 minutos el sitio estaba en línea. La primera vez que vi mi dominio funcionando me quedé mirando la pantalla unos segundos y no pude evitar hacer una captura.

Optimización en producción

Desplegar no es el final; quedan varios ajustes:
Pool de conexiones de Prisma:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // Obligatorio en Vercel serverless
  directUrl = env("DIRECT_URL")
}

En serverless cada invocación puede abrir una conexión nueva; directUrl permite reutilizar el pool.
Optimización de imágenes:

import Image from 'next/image'
<Image
  src="/avatar.jpg"
  alt="用户头像"
  width={100}
  height={100}
  // Next.js comprime y optimiza automáticamente
/>

El componente Image convierte a WebP y carga bajo demanda; la mejora es notable.
Puntuación final:

96
Lighthouse escritorio
92 móvil, SEO 100; de cero a producción en dos días

Con Lighthouse: 96 en escritorio, 92 en móvil, SEO 100. Ver ese resultado da mucha satisfacción: dos días, de cero a producción, con rendimiento real.

Extensiones y recursos de aprendizaje

El blog es un MVP; se pueden añadir muchas cosas:

  • Editor Markdown: integrar react-md-editor
  • Comentarios: Giscus (basado en GitHub Discussions)
  • Búsqueda: Algolia o búsqueda de texto completo con Prisma
  • Modo oscuro: con next-themes es sencillo
    Lo siguiente que haré es un editor Markdown; escribir posts será mucho más cómodo.
    Recursos recomendados:
  • Documentación oficial de Next.js — siempre la referencia
  • Documentación de Prisma — con leer Getting Started basta
  • El código completo de este proyecto — puedes clonarlo y ejecutarlo
    Créeme: hacerlo una vez vale más que leer diez documentos. La teoría ayuda, pero solo la práctica te lo deja interiorizado.

Resumen

Repasando lo aprendido en este fin de semana:

  1. La potencia de Server Actions: pueden sustituir la mayoría de API Routes; la eficiencia sube muchísimo
  2. Tipado seguro con Prisma: con TypeScript completo, codificar da tranquilidad
  3. Flexibilidad SSG/SSR: elegir la estrategia según el caso; rendimiento y SEO a la vez
  4. La simplicidad de Vercel: del código a producción en menos de 5 minutos
    Siendo honesto, lo que más me llevé no fue la técnica en sí, sino la confianza: el full stack no es tan imposible si te pones manos a la obra.
    Si quieres montar tu propio blog, no lo pienses más: empieza ya. Es normal tropezar; yo también consultaba documentación mientras escribía y caí en mil agujeros. Pero cuando ves tu trabajo en línea, todo compensa.
    Espero que este artículo te ahorre algunos desvíos. Si tienes dudas, comenta; intentaré responder.
    ¡Tengo ganas de ver lo que construyas!

Flujo completo para montar un blog de nivel producción con Next.js 15

Pasos desde el entorno hasta el despliegue, con Server Actions, Prisma y optimización SSG/SSR

Estimated time: PT16H

  1. 1

    Step 1: Configuración del entorno e inicialización del proyecto

    Crear proyecto Next.js 15:
  2. 2

    Step 2: Configurar schema de Prisma

    Diseñar el schema principal:
  3. 3

    Step 3: Implementar Server Actions principales

    Crear archivo de Server Actions:
  4. 4

    Step 4: Configurar autenticación de usuarios

    Instalar NextAuth.js:
  5. 5

    Step 5: Optimizar estrategia SSR/SSG

    Lista del blog con SSG+ISR: en app/blog/page.tsx, export const revalidate = 3600 (regenerar cada hora), prisma.post.findMany para posts publicados ordenados por createdAt descendente. La página genera HTML estático en el build y se regenera cada hora; el usuario recibe archivos estáticos (~200 ms). Detalle del post con SSR dinámico: en app/blog/[slug]/page.tsx cada visita consulta la base con prisma.post.findUnique({ where: { slug: params.slug } }); ideal para contenido que cambia a menudo (~800 ms). Comparativa: SSG ~200 ms, SSR ~800 ms, CSR puro ~1500 ms.
  6. 6

    Step 6: Desplegar en Vercel y optimizar

    Subir código a GitHub. Importar el proyecto en Vercel; detecta Next.js y configura casi todo. Variables de entorno: DATABASE_URL, GITHUB_ID, GITHUB_SECRET, NEXTAUTH_URL (https://your-domain.vercel.app), NEXTAUTH_SECRET (cadena aleatoria). Base de datos recomendada: Supabase PostgreSQL gratuito (500 MB/mes). Pool de Prisma: en prisma/schema.prisma añadir directUrl = env(“DIRECT_URL”) en datasource. Imágenes: componente Image de Next.js, WebP y carga bajo demanda. Tras Deploy, ~2 minutos hasta estar en línea. Lighthouse final: 96 escritorio, 92 móvil, SEO 100.

FAQ

¿Cuál es la diferencia entre Server Actions y API Routes en Next.js 15? ¿Cuándo usar cada uno?
Server Actions son como pedir comida a domicilio; API Routes son como ir tú al restaurante. Lo primero es más cómodo; lo segundo, más flexible.

Ventajas de Server Actions:
1) Un 30 % menos de código, sin escribir API Routes
2) Se llaman directamente desde el componente, sin useState, fetch ni onSubmit
3) Next.js gestiona serialización, peticiones de red y errores
4) Tipado seguro con TypeScript

Ejemplo de uso: añade 'use server' delante de la función y llámala desde el componente (<form action={createPost}>).

Ventajas de API Routes:
• Más flexibles para lógica de petición compleja
• Compatibles con middleware
• Ideales cuando necesitas respuestas HTTP personalizadas

En la mayoría de casos, Server Actions bastan; usa API Routes solo si necesitas un manejo HTTP más elaborado.
¿Cuánto mejora el rendimiento Turbopack? ¿Cómo se siente en el uso real?
Next.js 15 pasó Turbopack de experimental a estable.

Datos oficiales:
• Arranque del servidor local un 76,7 % más rápido
• Actualización de código un 96,3 % más rápida

Experiencia real:
• Mi proyecto tiene más de 30 componentes; con Webpack tardaba 7-8 segundos en arrancar; con Turbopack queda por debajo de 2 segundos
• Al cambiar código, el hot reload es casi instantáneo
• La mejora en la experiencia de desarrollo es enorme

Turbopack está escrito en Rust y es mucho más rápido que Webpack, sobre todo en proyectos grandes. Si sigues con Webpack, te recomiendo actualizar a Next.js 15 y probar Turbopack.
¿Por qué importa el patrón singleton de Prisma Client? ¿Qué pasa si no lo configuras?
El singleton de Prisma Client es la práctica recomendada oficialmente por Prisma.

El problema:
• En desarrollo, Next.js recarga en caliente y crea una nueva instancia de Prisma Client en cada recarga
• El pool de conexiones se agota y aparece el error Too many connections
• La primera vez que desplegué en Vercel olvidé configurarlo; a las pocas horas el sitio falló y pensé que era un ataque DDoS

Configuración correcta:
• Implementa un singleton global en lib/prisma.ts:
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}

Este código garantiza una sola instancia de Prisma en desarrollo sin afectar a producción. Recuérdalo: es la práctica recomendada por Prisma.
¿Cuál es la diferencia entre SSG, SSR e ISR? ¿Cómo elegir?
SSG (Static Site Generation):
• Genera HTML estático en el build; es lo más rápido (~200 ms)
• Ideal para páginas que cambian poco (lista del blog, página Acerca de)

SSR (Server-Side Rendering):
• Genera HTML en el servidor en cada petición
• Ideal para datos en tiempo real (detalle del post, perfil de usuario)
• Tiempo de carga ~800 ms

ISR (Incremental Static Regeneration):
• SSG con regeneración programada; combina velocidad de SSG y flexibilidad de SSR
• Ideal para contenido que se actualiza de vez en cuando (lista del blog cada hora)

CSR puro (Client-Side Rendering):
• Renderiza en el navegador; SEO débil; carga ~1500 ms
• No recomendado para blogs

Estrategia:
• Lista del blog: SSG+ISR (export const revalidate = 3600)
• Detalle del post: SSR dinámico
• Página Acerca de: SSG puro

Comparativa: SSG ~200 ms, SSR ~800 ms, CSR puro ~1500 ms; la diferencia es clara.
¿Por qué PostgreSQL y no MongoDB? ¿Cómo es el soporte de Prisma para PostgreSQL?
Razones para elegir PostgreSQL:
1) Prisma lo soporta mejor: tipado seguro, migraciones y optimización de consultas muy sólidas
2) Para datos con relaciones claras (posts, usuarios, comentarios), una base relacional encaja mejor
3) La búsqueda de texto completo de PostgreSQL es potente para buscar en el blog
4) Mayor estabilidad en producción y soporte transaccional completo

Ventajas de Prisma:
• Genera tipos TypeScript desde el schema; al escribir código tienes autocompletado y casi no te equivocas
• Antes usé Mongoose con MongoDB y los tipos los mantenía a mano; un descuido y aparecían bugs
• El tipado seguro de Prisma me da mucha tranquilidad al codificar

Si manejas relaciones complejas, PostgreSQL + Prisma es una excelente opción.
¿Qué tener en cuenta al desplegar Next.js en Vercel? ¿Cómo optimizar en producción?
Flujo de despliegue en Vercel:
1) Sube el código a GitHub
2) Importa el repositorio en Vercel; detecta Next.js y configura casi todo automáticamente
3) Configura variables de entorno (DATABASE_URL, GITHUB_ID, GITHUB_SECRET, NEXTAUTH_URL, NEXTAUTH_SECRET)
4) Pulsa Deploy; en unos 2 minutos el sitio está en línea

Optimización en producción:

1) Pool de conexiones de Prisma:
• En prisma/schema.prisma, en datasource añade directUrl = env("DIRECT_URL")
• En serverless cada invocación puede crear una conexión nueva; directUrl permite reutilizar el pool

2) Optimización de imágenes:
• Usa el componente Image de Next.js; convierte a WebP, carga bajo demanda; la mejora de rendimiento es notable

3) Base de datos en producción: Supabase PostgreSQL gratuito (500 MB/mes; de sobra para un blog personal)

Puntuación final:
• Con Lighthouse: 96 en escritorio, 92 en móvil, SEO 100
• Ver ese resultado da mucha satisfacción: dos días, de cero a producción, con rendimiento de nivel real

12 min de lectura · Publicado el: 24 nov 2025 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog