Introducción
Construir aplicaciones de nivel empresarial con Next.js requiere una cuidadosa consideración de la arquitectura, el rendimiento, la seguridad y la mantenibilidad. Esta guía proporciona una visión general completa de las mejores prácticas para desarrollar aplicaciones Next.js robustas que puedan escalar para satisfacer las demandas empresariales.1. Arquitectura y Escalabilidad
Arquitectura Modular
Organiza tu aplicación Next.js utilizando una arquitectura modular para mejorar la mantenibilidad y la escalabilidad:Copiar
/src
/app # App Router pages and layouts
/components # Reusable UI components
/ui # Base UI components
/features # Feature-specific components
/lib # Utility functions and shared code
/hooks # Custom React hooks
/services # External service integrations
/types # TypeScript type definitions
/styles # Global styles and theme configuration
/middleware # Next.js middleware
División de Código (Code Splitting) y Carga Diferida (Lazy Loading)
Next.js divide automáticamente el código por ruta, pero puedes optimizar aún más con importaciones dinámicas:Copiar
// Dynamic import with loading state
import dynamic from 'next/dynamic';
const DynamicDashboard = dynamic(() => import('@/components/Dashboard'), {
loading: () => <p>Cargando panel...</p>, //<- Traducido
ssr: false // Disable SSR if component relies on browser APIs
});
export default function Page() {
return <DynamicDashboard />;
}
Obtención de Datos Eficiente
Aprovecha los React Server Components para una obtención de datos eficiente:Copiar
// app/products/page.tsx
import { ProductList } from '@/components/ProductList';
import { getProducts } from '@/lib/products';
export default async function ProductsPage() {
// This runs on the server and doesn't send unnecessary data to the client
const products = await getProducts();
return <ProductList products={products} />;
}
Copiar
'use client';
import { useQuery } from '@tanstack/react-query';
function ProductsClient() {
const { data, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(res => res.json())
});
if (isLoading) return <div>Cargando...</div>; //<- Traducido
if (error) return <div>Error al cargar productos</div>; //<- Traducido
return (
<ul>
{data.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
Modelos de Despliegue
Despliegue Serverless
Los despliegues serverless (como Vercel) ofrecen escalado automático y reducen la carga operativa:Copiar
// next.config.js for optimized serverless deployment
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone', // Creates a standalone build optimized for containerized environments
}
module.exports = nextConfig;
Despliegue Contenerizado
Para despliegues en Kubernetes o Docker, usa el modo de salidastandalone:
Copiar
# Dockerfile for Next.js
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Create a non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy standalone build
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
Estrategias de Base de Datos y Caché
Selección de Base de Datos
Elige la base de datos adecuada según tus necesidades:- PostgreSQL: Para datos relacionales complejos con cumplimiento ACID
- MongoDB: Para esquemas flexibles y datos orientados a documentos
- Redis: Para caché y funcionalidades en tiempo real
- Supabase/Firebase: Para desarrollo rápido con autenticación incorporada
Implementación de Caché
Implementa caché multinivel:Copiar
// Route segment caching
export const revalidate = 3600; // Revalidate every hour
// Data cache with fetch
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 } // Cache for 1 hour
});
return res.json();
}
// Request memoization
import { cache } from 'react';
export const getUser = cache(async (id: string) => {
const user = await db.user.findUnique({ where: { id } });
return user;
});
2. Optimización del Rendimiento
Optimización de Imágenes
Usa el componenteImage de Next.js para optimización automática:
Copiar
import Image from 'next/image';
export default function ProductImage({ product }) {
return (
<div className="relative h-64 w-full">
<Image
src={product.imageUrl || "/placeholder.svg"}
alt={product.name}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={product.featured}
className="object-cover rounded-lg"
/>
</div>
);
}
Optimización de Fuentes
Optimiza las fuentes utilizando el sistema de fuentes incorporado de Next.js:Copiar
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
// Cambiado lang="en" a lang="es"
<html lang="es" className={`${inter.variable} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
);
}
Estrategias de Renderizado
Elige la estrategia de renderizado adecuada según tu contenido:Copiar
// Static page with generated static params
export async function generateStaticParams() {
const products = await getProducts();
return products.map((product) => ({
id: product.id,
}));
}
// Dynamic page with ISR
export const revalidate = 60; // Revalidate at most once per minute
// Dynamic page with on-demand revalidation
// pages/api/revalidate.js
export default async function handler(req, res) {
if (req.headers.authorization !== `Bearer ${process.env.REVALIDATION_TOKEN}`) {
// Traducido
return res.status(401).json({ message: 'Token inválido' });
}
try {
await res.revalidate('/products');
return res.json({ revalidated: true });
} catch (err) {
// Traducido
return res.status(500).send('Error al revalidar');
}
}
Monitorización del Rendimiento
Implementa la monitorización del rendimiento con herramientas como Web Vitals:Copiar
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitalsReporter() {
useReportWebVitals(metric => {
// Send to analytics
console.log(metric);
// Example: send to Google Analytics
const analyticsId = 'UA-XXXXX-Y';
const body = JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
});
navigator.sendBeacon(`https://www.google-analytics.com/collect?v=1&t=event&ec=Web%20Vitals&ea=${metric.name}&el=${metric.id}&ev=${metric.value}&tid=${analyticsId}`, body);
});
return null;
}
3. Mejores Prácticas de Seguridad
Validación de Entradas
Valida siempre las entradas del usuario tanto en el cliente como en el servidor:Copiar
// Server-side validation in a Server Action
'use server';
import { z } from 'zod';
const FormSchema = z.object({
// Traducido
email: z.string().email('Dirección de correo inválida'),
// Traducido
password: z.string().min(8, 'La contraseña debe tener al menos 8 caracteres'),
});
export async function createUser(prevState: any, formData: FormData) {
const validatedFields = FormSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
// Traducido
message: 'Campos faltantes. No se pudo crear el usuario.',
};
}
// Proceed with creating user...
}
Seguridad de API
Asegura tus rutas de API con autenticación adecuada y limitación de tasa (rate limiting):Copiar
// app/api/protected/route.ts
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { NextResponse } from 'next/server';
import { rateLimit } from '@/lib/rate-limit';
export async function GET(request: Request) {
const session = await getServerSession(authOptions);
if (!session) {
// Traducido
return NextResponse.json({ error: 'No autorizado' }, { status: 401 });
}
// Apply rate limiting
const limiter = rateLimit({
interval: 60 * 1000, // 1 minute
uniqueTokenPerInterval: 500,
});
try {
await limiter.check(10, session.user.id); // 10 requests per minute per user
} catch {
// Traducido
return NextResponse.json({ error: 'Límite de tasa excedido' }, { status: 429 });
}
// Process the request
// Traducido
return NextResponse.json({ data: 'Datos protegidos' });
}
Autenticación y Autorización
Implementa autenticación robusta con NextAuth.js:Copiar
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { compare } from 'bcrypt';
import { prisma } from '@/lib/prisma';
export const authOptions = {
providers: [
CredentialsProvider({
// Traducido
name: 'Credenciales',
credentials: {
// Traducido
email: { label: 'Email', type: 'email' },
// Traducido
password: { label: 'Contraseña', type: 'password' }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
const user = await prisma.user.findUnique({
where: { email: credentials.email }
});
if (!user) {
return null;
}
const isPasswordValid = await compare(credentials.password, user.password);
if (!isPasswordValid) {
return null;
}
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
};
}
})
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role;
}
return token;
},
async session({ session, token }) {
session.user.role = token.role;
return session;
}
},
pages: {
signIn: '/auth/signin',
error: '/auth/error',
},
session: {
strategy: 'jwt',
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Protección CSRF
Implementa protección CSRF (Cross-Site Request Forgery) para formularios:Copiar
'use client';
import { useState, useEffect } from 'react'; // Añadido useEffect que faltaba en original
import { getCsrfToken } from 'next-auth/react';
export default function ContactForm() {
const [csrfToken, setCsrfToken] = useState('');
// Añadido useEffect que faltaba en original
useEffect(() => {
async function fetchCsrfToken() {
const token = await getCsrfToken();
setCsrfToken(token || '');
}
fetchCsrfToken();
}, []);
return (
<form method="post" action="/api/contact">
<input name="csrfToken" type="hidden" value={csrfToken} />
{/* Form fields */}
</form>
);
}
Política de Seguridad de Contenido (CSP)
Implementa una Política de Seguridad de Contenido estricta a través de middleware:Copiar
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Add security headers
response.headers.set('Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://analytics.example.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https://images.example.com; " +
"font-src 'self'; " +
"connect-src 'self' https://api.example.com; " +
"frame-src 'none'; " +
"object-src 'none';"
);
response.headers.set('X-XSS-Protection', '1; mode=block');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
return response;
}
export const config = {
matcher: '/((?!api/auth|_next/static|_next/image|favicon.ico).*)',
};
4. Calidad del Código y Mantenibilidad
Integración con TypeScript
Usa TypeScript para seguridad de tipos y una mejor experiencia de desarrollo:Copiar
// types/index.ts
export interface User {
id: string;
name: string;
email: string;
// Traducido 'admin' | 'user' a 'admin' | 'usuario' - CORRECCIÓN: Mantener original
role: 'admin' | 'user';
createdAt: Date;
}
export interface Product {
id: string;
name: string;
description: string;
price: number;
imageUrl: string;
category: string;
featured: boolean;
}
Linting y Formateo
Configura ESLint y Prettier para un estilo de código consistente:Copiar
// .eslintrc.json
{
"extends": [
"next/core-web-vitals",
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"plugins": ["@typescript-eslint"],
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"react/no-unescaped-entities": "off",
"@typescript-eslint/no-explicit-any": "warn",
"react-hooks/exhaustive-deps": "warn"
}
}
Copiar
// .prettierrc
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"tabWidth": 2,
"printWidth": 100
}
Estrategia de Pruebas
Implementa una estrategia de pruebas completa:Copiar
// Unit test with Jest and React Testing Library
// __tests__/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from '@/components/ui/Button';
describe('Button component', () => {
it('renders correctly', () => {
// Traducido
render(<Button>Haz clic</Button>);
// Traducido
expect(screen.getByRole('button', { name: /haz clic/i })).toBeInTheDocument();
});
it('calls onClick handler when clicked', () => {
const handleClick = jest.fn();
// Traducido
render(<Button onClick={handleClick}>Haz clic</Button>);
// Traducido
fireEvent.click(screen.getByRole('button', { name: /haz clic/i }));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
Copiar
// Integration test with Cypress
// cypress/e2e/authentication.cy.ts
describe('Authentication', () => {
it('should allow a user to sign in', () => {
cy.visit('/auth/signin');
cy.get('input[name="email"]').type('[email protected]');
cy.get('input[name="password"]').type('password123');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
// Traducido
cy.contains('Bienvenido de nuevo').should('be.visible');
});
});
Documentación de Componentes
Documenta tus componentes con Storybook:Copiar
// stories/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from '@/components/ui/Button';
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'],
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
// Traducido
children: 'Botón',
variant: 'default',
},
};
export const Destructive: Story = {
args: {
// Traducido
children: 'Eliminar',
variant: 'destructive',
},
};
5. Despliegue y DevOps
Pipeline de CI/CD
Configura un pipeline de CI/CD (Integración Continua / Despliegue Continuo) con GitHub Actions:Copiar
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run test
build:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: build
path: .next
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Download build artifact
uses: actions/download-artifact@v3
with:
name: build
path: .next
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
Configuración Multi-Entorno
Configura ajustes específicos para cada entorno:Copiar
.env # Default environment variables
.env.local # Local overrides (not committed)
.env.development # Development environment
.env.test # Test environment
.env.production # Production environment
Monitorización y Registro (Logging)
Implementa monitorización de aplicaciones con Sentry:Copiar
// app/monitoring.ts - CORRECCIÓN: Nombre de archivo diferente al original
// Original: No tenía un archivo específico, sino la config de Sentry.
// Revertir a una traducción directa del fragmento original.
// (El fragmento original en inglés no era una configuración completa,
// solo un ejemplo de inicialización y captura de errores).
// Manteniendo el código original, aunque no sea una config completa:
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
environment: process.env.NODE_ENV,
// El original tenía un objeto Integrations.Http, mantenerlo
integrations: [
// @ts-ignore - Puede requerir ignorar si Sentry o las types cambiaron
new Sentry.Integrations.Http({ tracing: true }),
],
});
// Instrument Next.js error handling
export function onRequestError({ error, request, context }) {
Sentry.captureException(error, {
extra: {
requestUrl: request.url,
context,
},
});
}
export function register() {
// Initialize any other monitoring tools
}
6. Gestión de Estado y Obtención de Datos
Gestión del Estado del Servidor
Usa React Query para la gestión del estado del servidor:Copiar
// lib/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
retry: 1,
refetchOnWindowFocus: process.env.NODE_ENV === 'production',
},
},
}));
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Gestión del Estado del Cliente
Usa Zustand para la gestión del estado del lado del cliente:Copiar
// store/useCartStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartStore {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
totalItems: () => number;
totalPrice: () => number;
}
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) => set((state) => {
const existingItem = state.items.find((i) => i.id === item.id);
if (existingItem) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) => set((state) => ({
items: state.items.filter((i) => i.id !== id),
})),
updateQuantity: (id, quantity) => set((state) => ({
items: state.items.map((i) =>
i.id === id ? { ...i, quantity } : i
),
})),
clearCart: () => set({ items: [] }),
totalItems: () => get().items.reduce((acc, item) => acc + item.quantity, 0),
totalPrice: () => get().items.reduce((acc, item) => acc + (item.price * item.quantity), 0),
}),
{
name: 'cart-storage',
}
)
);
Patrones de Obtención de Datos
Implementa patrones eficientes para la obtención de datos:Copiar
// Parallel data fetching
async function ParallelDataFetching() {
// Fetch data in parallel
const productsPromise = getProducts();
const categoriesPromise = getCategories();
const featuredPromise = getFeaturedProducts();
// Wait for all promises to resolve
const [products, categories, featured] = await Promise.all([
productsPromise,
categoriesPromise,
featuredPromise,
]);
return (
<div>
<FeaturedProducts products={featured} />
<CategoryNav categories={categories} />
<ProductGrid products={products} />
</div>
);
}
// Sequential data fetching (when one request depends on another)
async function SequentialDataFetching() {
// First fetch categories
const categories = await getCategories();
// Then fetch products for the first category
const products = categories.length > 0
? await getProductsByCategory(categories[0].id)
: [];
return (
<div>
<CategoryNav categories={categories} />
<ProductGrid products={products} />
</div>
);
}
7. Manejo de Errores y Monitorización
Manejo Global de Errores
Implementa límites de error globales:Copiar
// app/global-error.tsx
'use client';
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log the error to Sentry
Sentry.captureException(error);
}, [error]);
return (
<html>
<body>
<div className="flex min-h-screen flex-col items-center justify-center p-4 text-center">
{/* Traducido */}
<h1 className="text-4xl font-bold mb-4">¡Algo salió mal!</h1>
{/* Traducido */}
<p className="mb-8 text-gray-600">
Hemos sido notificados sobre este problema y estamos trabajando para solucionarlo.
</p>
<button
onClick={() => reset()}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
>
{/* Traducido */}
Intentar de nuevo
</button>
</div>
</body>
</html>
);
}
Manejo de Errores Específico de Ruta
Implementa límites de error específicos para rutas:Copiar
// app/dashboard/error.tsx
'use client';
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';
import { Button } from '@/components/ui/button'; // Corregido import, original era minúscula
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log the error to Sentry
Sentry.captureException(error);
}, [error]);
return (
<div className="p-8 text-center">
{/* Traducido */}
<h2 className="text-2xl font-bold mb-4">Error en el Panel</h2>
{/* Traducido */}
<p className="mb-4 text-gray-600">
Ocurrió un error al cargar el panel.
</p>
<div className="flex justify-center gap-4">
{/* Traducido */}
<Button onClick={() => reset()}>Intentar de nuevo</Button>
<Button variant="outline" onClick={() => window.location.href = '/'}>
{/* Traducido */}
Ir al Inicio
</Button>
</div>
</div>
);
}
Registro Estructurado (Logging)
Implementa logging estructurado:Copiar
// lib/logger.ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
interface LogEntry {
level: LogLevel;
message: string;
timestamp: string;
context?: Record<string, any>;
}
class Logger {
private context: Record<string, any> = {};
constructor(context: Record<string, any> = {}) {
this.context = context;
}
private log(level: LogLevel, message: string, additionalContext: Record<string, any> = {}) {
const timestamp = new Date().toISOString();
const entry: LogEntry = {
level,
message,
timestamp,
context: {
...this.context,
...additionalContext,
},
};
// In development, log to console
if (process.env.NODE_ENV === 'development') {
console[level === 'debug' ? 'log' : level](JSON.stringify(entry, null, 2));
} else {
// In production, send to logging service
// Example: send to Datadog, Loggly, etc.
// For now, still log to console in production
console[level === 'debug' ? 'log' : level](JSON.stringify(entry));
}
return entry;
}
debug(message: string, context?: Record<string, any>) {
return this.log('debug', message, context);
}
info(message: string, context?: Record<string, any>) {
return this.log('info', message, context);
}
warn(message: string, context?: Record<string, any>) {
return this.log('warn', message, context);
}
error(message: string, error?: Error, context?: Record<string, any>) {
return this.log('error', message, {
...context,
error: error ? {
message: error.message,
stack: error.stack,
name: error.name,
} : undefined,
});
}
withContext(additionalContext: Record<string, any>) {
return new Logger({
...this.context,
...additionalContext,
});
}
}
export const logger = new Logger({
service: 'next-app',
environment: process.env.NODE_ENV,
});
8. Accesibilidad (a11y) e Internacionalización (i18n)
Implementación de Accesibilidad (a11y)
Asegura que tu aplicación sea accesible:Copiar
// components/ui/Button.tsx
import { forwardRef } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'underline-offset-4 hover:underline text-primary',
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-9 px-3 rounded-md',
lg: 'h-11 px-8 rounded-md',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };
Implementación de Atributos ARIA
Copiar
// components/ui/Dialog.tsx
'use client';
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cn } from '@/lib/utils';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
// Traducido
aria-label="Cerrar diálogo"
>
<X className="h-4 w-4" />
{/* Traducido */}
<span className="sr-only">Cerrar</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
// Mantener exportación original
export { Dialog, DialogTrigger, DialogContent, DialogClose };
Internacionalización (i18n)
Implementa internacionalización connext-intl:
Copiar
// middleware.ts
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// A list of all locales that are supported
locales: ['en', 'es', 'fr', 'de'],
// If this locale is matched, pathnames work without a prefix (e.g. `/about`)
defaultLocale: 'en',
// Domains can be used to match specific locales
domains: [
{
domain: 'example.com',
defaultLocale: 'en'
},
{
domain: 'example.es',
defaultLocale: 'es'
}
]
});
export const config = {
// Skip all paths that should not be internationalized
matcher: ['/((?!api|_next|.*\\..*).*)']
};
Copiar
// messages/en.json (Archivo de inglés original)
{
"Index": {
"title": "Hello world!",
"description": "This is a sample application"
},
"Navigation": {
"home": "Home",
"about": "About",
"products": "Products",
"contact": "Contact"
},
"Auth": {
"signIn": "Sign In",
"signUp": "Sign Up",
"email": "Email",
"password": "Password",
"forgotPassword": "Forgot Password?"
}
}
// messages/es.json (Archivo de español traducido)
{
"Index": {
"title": "¡Hola mundo!",
"description": "Esta es una aplicación de ejemplo"
},
"Navigation": {
"home": "Inicio",
"about": "Acerca de",
"products": "Productos",
"contact": "Contacto"
},
"Auth": {
"signIn": "Iniciar Sesión",
"signUp": "Registrarse",
"email": "Correo electrónico",
"password": "Contraseña",
"forgotPassword": "¿Olvidaste tu contraseña?"
}
}
Copiar
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { notFound } from 'next/navigation';
export function generateStaticParams() {
return [{ locale: 'en' }, { locale: 'es' }, { locale: 'fr' }, { locale: 'de' }];
}
export default async function LocaleLayout({
children,
params: { locale }
}: {
children: React.ReactNode;
params: { locale: string };
}) {
let messages;
try {
messages = (await import(`../../messages/${locale}.json`)).default;
} catch (error) {
notFound();
}
return (
// Provider necesita lang={locale} en <html>, no aquí
// El html tag está en el RootLayout superior usualmente
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
);
}
Copiar
// app/[locale]/page.tsx
import { useTranslations } from 'next-intl';
export default function Index() {
const t = useTranslations('Index');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}
9. Colaboración en Equipo y Flujo de Trabajo de Desarrollo
Flujo de Trabajo con Git
Implementa un flujo de trabajo robusto con Git:Copiar
# Feature branch workflow
git checkout -b feature/user-authentication
# Make changes
git add .
git commit -m "feat: implement user authentication"
# Push to remote
git push -u origin feature/user-authentication
# Create pull request
# After review and approval, merge to main
Commits Convencionales
Usa Commits Convencionales para mejor generación de changelogs:Copiar
feat: add user authentication
fix: resolve issue with password reset
docs: update README with setup instructions
style: format code according to style guide
refactor: simplify product filtering logic
test: add tests for cart functionality
chore: update dependencies
Directrices para la Revisión de Código
Establece directrices claras para la revisión de código:- Funcionalidad: ¿El código funciona como se espera?
- Seguridad: ¿Hay alguna vulnerabilidad de seguridad?
- Rendimiento: ¿Está el código optimizado para el rendimiento?
- Mantenibilidad: ¿Es el código fácil de entender y mantener?
- Pruebas: ¿Hay suficientes pruebas?
- Documentación: ¿Está el código bien documentado?
Gestión de Dependencias
Gestiona las dependencias de forma eficaz:Copiar
// package.json
{
"name": "enterprise-nextjs-app",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest",
"test:watch": "jest --watch",
"e2e": "cypress run",
"e2e:open": "cypress open",
"format": "prettier --write .",
"prepare": "husky install",
"check-deps": "npx npm-check-updates"
},
"dependencies": {
"@sentry/nextjs": "^7.64.0", // Mantener versiones originales
"next": "^14.0.0", // Mantener versión original
"next-auth": "^4.24.5",
"next-intl": "^3.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^3.22.2",
"zustand": "^4.4.1"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.4",
"@types/node": "^20.6.0",
"@types/react": "^18.2.21",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"cypress": "^13.1.0",
"eslint": "^8.49.0",
"eslint-config-next": "^14.0.0", // Mantener versión original
"eslint-config-prettier": "^9.0.0",
"husky": "^8.0.3",
"jest": "^29.6.4",
"jest-environment-jsdom": "^29.6.4",
"lint-staged": "^14.0.1",
"prettier": "^3.0.3",
"typescript": "^5.2.2"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,css,md}": [
"prettier --write"
]
}
}
10. Temas Avanzados
Configuración de Monorepo con Turborepo
Configura un monorepo con Turborepo:Copiar
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"]
}
}
}
Copiar
/apps
/web # Main Next.js application
/admin # Admin dashboard
/docs # Documentation site
/packages
/ui # Shared UI components
/utils # Shared utilities
/api-client # API client library
/config # Shared configuration
/tsconfig # Shared TypeScript configuration
Micro-Frontends
Implementa micro-frontends con Module Federation:Copiar
// next.config.js
const { NextFederationPlugin } = require('@module-federation/nextjs-mf');
module.exports = {
webpack(config, options) {
const { isServer } = options;
config.plugins.push(
new NextFederationPlugin({
name: 'main',
remotes: {
shop: `shop@${process.env.SHOP_URL}/_next/static/${isServer ? 'ssr' : 'chunks'}/remoteEntry.js`,
blog: `blog@${process.env.BLOG_URL}/_next/static/${isServer ? 'ssr' : 'chunks'}/remoteEntry.js`,
},
filename: 'static/chunks/remoteEntry.js',
exposes: {
'./Header': './components/Header',
'./Footer': './components/Footer',
'./AuthContext': './contexts/AuthContext',
},
shared: {
react: {
singleton: true,
requiredVersion: false,
},
'react-dom': {
singleton: true,
requiredVersion: false,
},
},
})
);
// Añadido faltante en original para Module Federation
if (!isServer) {
config.output.publicPath = "auto";
}
// Fin añadido faltante
return config;
},
};
Computación en el Borde (Edge Computing)
Aprovecha la Computación en el Borde para rendimiento global:Copiar
// app/api/geo/route.ts
export const runtime = 'edge';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const ip = request.headers.get('x-forwarded-for') || 'unknown';
// Get user's country from request headers
const country = request.headers.get('x-vercel-ip-country') || 'unknown';
const region = request.headers.get('x-vercel-ip-country-region') || 'unknown';
const city = request.headers.get('x-vercel-ip-city') || 'unknown';
return Response.json({
ip,
geo: {
country,
region,
city,
},
timestamp: new Date().toISOString(),
});
}
Funciones Serverless
Implementa funciones serverless para tareas específicas:Copiar
// app/api/generate-thumbnail/route.ts
import { put } from '@vercel/blob';
import { NextResponse } from 'next/server';
import sharp from 'sharp';
export async function POST(request: Request) {
try {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
// Traducido
return NextResponse.json(
{ error: 'No se proporcionó ningún archivo' },
{ status: 400 }
);
}
// Convert file to buffer
const buffer = Buffer.from(await file.arrayBuffer());
// Generate thumbnail
const thumbnail = await sharp(buffer)
.resize(200, 200, { fit: 'inside' })
.toBuffer();
// Upload to Vercel Blob
const { url } = await put(`thumbnails/${Date.now()}-${file.name}`, thumbnail, {
access: 'public',
contentType: 'image/jpeg', // Asumiendo que sharp genera jpeg por defecto aquí
});
return NextResponse.json({ url });
} catch (error) {
console.error('Error generating thumbnail:', error);
// Traducido
return NextResponse.json(
{ error: 'Fallo al generar la miniatura' },
{ status: 500 }
);
}
}