Siguiente.js Cheatsheet
■h1 títuloSiguiente.js - El Marco de Reacción para la Producción "Clase de inscripción" Next.js es un marco React que le da bloques de construcción para crear aplicaciones web. Por marco, nos referimos a Next.js maneja la herramienta y configuración necesarias para React, y proporciona estructura adicional, características y optimizaciones para su aplicación. ▪/p] ■/div titulada
########################################################################################################################################################################################################################################################## Copiar todos los comandos
########################################################################################################################################################################################################################################################## Generar PDF seleccionado/button
■/div titulada ■/div titulada
Cuadro de contenidos
- Instalación
- Creación del proyecto
- Estructura del proyecto
- Routing
- Pagos y diseños
- API Routes
- Data Fetching
- Styling
- Optimización de imagen
- Perfeccionamiento Optimización
- Authentication
- Database Integration
- Deployment
- Testing
- Solucionando
- Las mejores prácticas
Instalación
Prerrequisitos
# Node.js version requirements
node --version # Should be 16.8 or later
# Check npm version
npm --version
# Update npm if needed
npm install -g npm@latest
Crear Siguiente.js App
# Create new Next.js app
npx create-next-app@latest my-app
# Create with TypeScript
npx create-next-app@latest my-app --typescript
# Create with specific template
npx create-next-app@latest my-app --example with-tailwindcss
# Interactive creation
npx create-next-app@latest
Instalación manual
# Create project directory
mkdir my-nextjs-app
cd my-nextjs-app
# Initialize package.json
npm init -y
# Install Next.js, React, and React DOM
npm install next react react-dom
# Install TypeScript (optional)
npm install --save-dev typescript @types/react @types/node
Paquete.json Scripts
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"export": "next export"
}
}
Creación de proyectos
Plantillas disponibles
# Official examples
npx create-next-app@latest --example api-routes
npx create-next-app@latest --example with-typescript
npx create-next-app@latest --example with-tailwindcss
npx create-next-app@latest --example with-styled-components
npx create-next-app@latest --example with-material-ui
npx create-next-app@latest --example with-redux
npx create-next-app@latest --example with-apollo
npx create-next-app@latest --example with-prisma
npx create-next-app@latest --example with-supabase
npx create-next-app@latest --example with-firebase
Development Server
# Start development server
npm run dev
# Start on specific port
npm run dev -- -p 3001
# Start with turbo mode (faster builds)
npm run dev -- --turbo
# Start with experimental features
npm run dev -- --experimental-app
Construcción y producción
# Build for production
npm run build
# Start production server
npm run start
# Export static site
npm run export
# Analyze bundle
npm run build -- --analyze
Estructura del proyecto
App Router Structure (Next.js 13+)
my-nextjs-app/
├── app/ # App Router (new)
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ ├── loading.tsx # Loading UI
│ ├── error.tsx # Error UI
│ ├── not-found.tsx # 404 page
│ ├── about/
│ │ └── page.tsx # /about route
│ ├── blog/
│ │ ├── page.tsx # /blog route
│ │ └── [slug]/
│ │ └── page.tsx # /blog/[slug] route
│ └── api/
│ └── users/
│ └── route.ts # API endpoint
├── components/ # Reusable components
├── lib/ # Utility functions
├── public/ # Static assets
├── styles/ # Additional styles
├── next.config.js # Next.js configuration
├── package.json
└── tsconfig.json
Estructura del Router (Legacy)
my-nextjs-app/
├── pages/ # Pages Router (legacy)
│ ├── _app.tsx # Custom App component
│ ├── _document.tsx # Custom Document
│ ├── index.tsx # Home page (/)
│ ├── about.tsx # About page (/about)
│ ├── blog/
│ │ ├── index.tsx # Blog index (/blog)
│ │ └── [slug].tsx # Dynamic route (/blog/[slug])
│ └── api/
│ └── users.ts # API endpoint (/api/users)
├── components/
├── lib/
├── public/
├── styles/
├── next.config.js
└── package.json
Routing
App Router (Next.js 13+)
Routing basado en archivos
# Route structure
app/
├── page.tsx # / (root)
├── about/page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/page.tsx # /blog/[slug]
├── shop/
│ └── [category]/
│ └── [product]/
│ └── page.tsx # /shop/[category]/[product]
└── (dashboard)/ # Route group (doesn't affect URL)
├── analytics/page.tsx # /analytics
└── settings/page.tsx # /settings
Rutas dinámicas
// app/blog/[slug]/page.tsx
interface PageProps {
params: { slug: string };
| searchParams: { [key: string]: string | string[] | undefined }; |
}
export default function BlogPost({ params, searchParams }: PageProps) {
return <h1>Post: {params.slug}</h1>;
}
// Generate static params
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json());
return posts.map((post: any) => ({
slug: post.slug,
}));
}
Recorridos
// app/shop/[...slug]/page.tsx
interface PageProps {
params: { slug: string[] };
}
export default function Shop({ params }: PageProps) {
// /shop/a/b/c -> params.slug = ['a', 'b', 'c']
return <div>Shop: {params.slug.join('/')}</div>;
}
Navegación
// Client-side navigation
import Link from 'next/link';
import { useRouter } from 'next/navigation';
function Navigation() {
const router = useRouter();
return (
<nav>
{/* Static link */}
<Link href="/about">About</Link>
{/* Dynamic link */}
<Link href={`/blog/${post.slug}`}>Read Post</Link>
{/* Programmatic navigation */}
<button onClick={() => router.push('/dashboard')}>
Go to Dashboard
</button>
{/* Replace current entry */}
<button onClick={() => router.replace('/login')}>
Login
</button>
{/* Go back */}
<button onClick={() => router.back()}>
Go Back
</button>
</nav>
);
}
Grupos de ruta y rutas paralelas
# Route groups (don't affect URL structure)
app/
├── (marketing)/
│ ├── about/page.tsx # /about
│ └── contact/page.tsx # /contact
├── (shop)/
│ ├── products/page.tsx # /products
│ └── cart/page.tsx # /cart
└── page.tsx # /
# Parallel routes
app/
├── @analytics/
│ └── page.tsx
├── @team/
│ └── page.tsx
├── layout.tsx # Can render both slots
└── page.tsx
Páginas y diseños
Root Layout
// app/layout.tsx
import './globals.css';
export const metadata = {
title: 'My App',
description: 'Generated by Next.js',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>Navigation</nav>
</header>
<main>{children}</main>
<footer>Footer</footer>
</body>
</html>
);
}
Diseños anidados
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<aside>
<nav>Dashboard Navigation</nav>
</aside>
<main>{children}</main>
</div>
);
}
Componentes de página
// app/page.tsx
export default function HomePage() {
return (
<div>
<h1>Welcome to Next.js!</h1>
<p>This is the home page.</p>
</div>
);
}
// With metadata
export const metadata = {
title: 'Home | My App',
description: 'Welcome to my Next.js application',
};
Estados de carga y error
// app/loading.tsx
export default function Loading() {
return (
<div className="loading">
<div className="spinner">Loading...</div>
</div>
);
}
// app/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="error">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/not-found.tsx
export default function NotFound() {
return (
<div className="not-found">
<h2>Not Found</h2>
<p>Could not find requested resource</p>
</div>
);
}
API Routes
App Router API Routes
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET /api/users
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('query');
// Fetch users from database
const users = await fetchUsers(query);
return NextResponse.json(users);
}
// POST /api/users
export async function POST(request: NextRequest) {
const body = await request.json();
// Create user
const user = await createUser(body);
return NextResponse.json(user, { status: 201 });
}
// PUT /api/users
export async function PUT(request: NextRequest) {
const body = await request.json();
// Update user
const user = await updateUser(body);
return NextResponse.json(user);
}
// DELETE /api/users
export async function DELETE(request: NextRequest) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
await deleteUser(id);
return new NextResponse(null, { status: 204 });
}
Rutas dinámicas de API
// app/api/users/[id]/route.ts
interface RouteParams {
params: { id: string };
}
export async function GET(
request: NextRequest,
{ params }: RouteParams
) {
const user = await fetchUser(params.id);
if (!user) {
return new NextResponse('User not found', { status: 404 });
}
return NextResponse.json(user);
}
export async function DELETE(
request: NextRequest,
{ params }: RouteParams
) {
await deleteUser(params.id);
return new NextResponse(null, { status: 204 });
}
Middleware
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Authentication check
const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Add custom headers
const response = NextResponse.next();
response.headers.set('X-Custom-Header', 'value');
return response;
}
export const config = {
matcher: [
| '/((?!api | _next/static | _next/image | favicon.ico).*)', |
],
};
Manejo de errores de API
// lib/api-error.ts
export class ApiError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
}
// app/api/users/route.ts
export async function GET() {
try {
const users = await fetchUsers();
return NextResponse.json(users);
} catch (error) {
if (error instanceof ApiError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.statusCode }
);
}
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}
Datos obtenidos
Componentes de servidor (por defecto)
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
// Revalidate every hour
next: { revalidate: 3600 }
});
if (!res.ok) {
throw new Error('Failed to fetch posts');
}
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Posts</h1>
{posts.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
Componentes del cliente
// components/PostList.tsx
'use client';
import { useState, useEffect } from 'react';
export default function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchPosts() {
try {
const res = await fetch('/api/posts');
const data = await res.json();
setPosts(data);
} catch (error) {
console.error('Error fetching posts:', error);
} finally {
setLoading(false);
}
}
fetchPosts();
}, []);
if (loading) return <div>Loading...</div>;
return (
<div>
{posts.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
);
}
Static Site Generation (SSG)
// app/posts/[slug]/page.tsx
interface Post {
slug: string;
title: string;
content: string;
}
// Generate static params at build time
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json());
return posts.map((post: Post) => ({
slug: post.slug,
}));
}
// Generate metadata
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(res => res.json());
return {
title: post.title,
description: post.excerpt,
};
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(res => res.json());
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Regeneración Estatica Incremental (ISR)
// Revalidate every 60 seconds
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
});
return res.json();
}
// On-demand revalidation
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret');
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
}
try {
await revalidatePath('/posts');
return NextResponse.json({ revalidated: true });
} catch (err) {
return NextResponse.json({ message: 'Error revalidating' }, { status: 500 });
}
}
Styling
CSS Módulos
/* styles/Home.module.css */
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
text-align: center;
}
// components/Home.tsx
import styles from '../styles/Home.module.css';
export default function Home() {
return (
<div className={styles.container}>
<main className={styles.main}>
<h1 className={styles.title}>Welcome to Next.js!</h1>
</main>
</div>
);
}
Tailwind CSS
# Install Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#64748b',
},
},
},
plugins: [],
};
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn-primary {
@apply bg-primary text-white px-4 py-2 rounded hover:bg-blue-600;
}
}
Componentes de estilo
# Install styled-components
npm install styled-components
npm install -D @types/styled-components
// components/Button.tsx
import styled from 'styled-components';
const StyledButton = styled.button<{ variant?: 'primary' | 'secondary' }>`
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 500;
${props => props.variant === 'primary' && `
background-color: #3b82f6;
color: white;
&:hover {
background-color: #2563eb;
}
`}
${props => props.variant === 'secondary' && `
background-color: #6b7280;
color: white;
&:hover {
background-color: #4b5563;
}
`}
`;
export default function Button({ children, variant = 'primary', ...props }) {
return (
<StyledButton variant={variant} {...props}>
{children}
</StyledButton>
);
}
CSS-en-JS con Emoción
# Install Emotion
npm install @emotion/react @emotion/styled
// components/Card.tsx
import styled from '@emotion/styled';
import { css } from '@emotion/react';
const Card = styled.div`
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 1rem;
margin: 1rem 0;
`;
const cardTitle = css`
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
color: #1f2937;
`;
export default function ProductCard({ title, description }) {
return (
<Card>
<h3 css={cardTitle}>{title}</h3>
<p>{description}</p>
</Card>
);
}
Optimización de imagen
Next.js Image Component
import Image from 'next/image';
export default function Gallery() {
return (
<div>
{/* Static image */}
<Image
src="/hero.jpg"
alt="Hero image"
width={800}
height={600}
priority // Load immediately
/>
{/* Dynamic image */}
<Image
src={`https://api.example.com/images/${imageId}`}
alt="Dynamic image"
width={400}
height={300}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
{/* Responsive image */}
<Image
src="/responsive.jpg"
alt="Responsive image"
fill
style={{ objectFit: 'cover' }}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
);
}
Configuración de imagen
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['example.com', 'api.example.com'],
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com',
port: '',
pathname: '/images/**',
},
],
formats: ['image/webp', 'image/avif'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
};
module.exports = nextConfig;
Cargador de imagen personalizado
// lib/imageLoader.ts
export default function cloudinaryLoader({ src, width, quality }) {
| const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality | | 'auto'}`]; |
return `https://res.cloudinary.com/demo/image/fetch/${params.join(',')}/${src}`;
}
// Usage
<Image
loader={cloudinaryLoader}
src="/sample.jpg"
alt="Sample"
width={500}
height={300}
/>
Optimización del rendimiento
Bundle Analysis
# Install bundle analyzer
npm install --save-dev @next/bundle-analyzer
# Configure in next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// Your Next.js config
});
# Analyze bundle
ANALYZE=true npm run build
Código de división
// Dynamic imports
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false, // Disable server-side rendering
});
// Conditional loading
const ConditionalComponent = dynamic(
() => import('../components/ConditionalComponent'),
{ ssr: false }
);
export default function Page() {
const [showComponent, setShowComponent] = useState(false);
return (
<div>
<button onClick={() => setShowComponent(true)}>
Load Component
</button>
{showComponent && <ConditionalComponent />}
</div>
);
}
Lazy Cargando
import { lazy, Suspense } from 'react';
const LazyChart = lazy(() => import('../components/Chart'));
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading chart...</div>}>
<LazyChart />
</Suspense>
</div>
);
}
Supervisión de la ejecución
// lib/analytics.ts
export function reportWebVitals(metric) {
switch (metric.name) {
case 'CLS':
case 'FID':
case 'FCP':
case 'LCP':
case 'TTFB':
// Send to analytics service
analytics.track('Web Vital', {
name: metric.name,
value: metric.value,
id: metric.id,
});
break;
default:
break;
}
}
// app/layout.tsx
import { reportWebVitals } from '../lib/analytics';
export { reportWebVitals };
Estrategias de producción
// Static data caching
const getData = cache(async () => {
const res = await fetch('https://api.example.com/data');
return res.json();
});
// Request memoization
import { unstable_cache } from 'next/cache';
const getCachedData = unstable_cache(
async (id) => {
const data = await fetchData(id);
return data;
},
['data-cache'],
{ revalidate: 3600 } // 1 hour
);
```_
## Autenticación
### NextAuth.js Setup
```bash
# Install NextAuth.js
npm install next-auth
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import CredentialsProvider from 'next-auth/providers/credentials';
const handler = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials) {
// Verify credentials
const user = await verifyCredentials(credentials);
return user ? { id: user.id, email: user.email } : null;
}
})
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
session.user.id = token.id;
return session;
},
},
pages: {
signIn: '/auth/signin',
signUp: '/auth/signup',
},
});
export { handler as GET, handler as POST };
Gestión del período de sesiones
// components/AuthProvider.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
export default function AuthProvider({
children,
session,
}: {
children: React.ReactNode;
session: any;
}) {
return <SessionProvider session={session}>{children}</SessionProvider>;
}
// app/layout.tsx
import AuthProvider from '../components/AuthProvider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}
Rutas protegidas
// components/ProtectedRoute.tsx
import { useSession } from 'next-auth/react';
import { redirect } from 'next/navigation';
export default function ProtectedRoute({
children,
}: {
children: React.ReactNode;
}) {
const { data: session, status } = useSession();
if (status === 'loading') {
return <div>Loading...</div>;
}
if (!session) {
redirect('/auth/signin');
}
return <>{children}</>;
}
// Server-side protection
import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';
export default async function ProtectedPage() {
const session = await getServerSession();
if (!session) {
redirect('/auth/signin');
}
return <div>Protected content</div>;
}
Integración de bases de datos
Prisma Setup
# Install Prisma
npm install prisma @prisma/client
npx prisma init
```_
```prisma
// 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?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// 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;
Operaciones de base de datos
// lib/posts.ts
import { prisma } from './prisma';
export async function getPosts() {
return await prisma.post.findMany({
include: {
author: {
select: {
name: true,
email: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
});
}
export async function createPost(data: {
title: string;
content: string;
authorId: string;
}) {
return await prisma.post.create({
data,
include: {
author: true,
},
});
}
export async function updatePost(id: string, data: {
title?: string;
content?: string;
published?: boolean;
}) {
return await prisma.post.update({
where: { id },
data,
});
}
export async function deletePost(id: string) {
return await prisma.post.delete({
where: { id },
});
}
API Integration
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getPosts, createPost } from '../../../lib/posts';
export async function GET() {
try {
const posts = await getPosts();
return NextResponse.json(posts);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const post = await createPost(body);
return NextResponse.json(post, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create post' },
{ status: 500 }
);
}
}
Despliegue
Despliegue de Vercel
# Install Vercel CLI
npm install -g vercel
# Deploy to Vercel
vercel
# Deploy to production
vercel --prod
# Set environment variables
vercel env add VARIABLE_NAME
// vercel.json
{
"framework": "nextjs",
"buildCommand": "npm run build",
"devCommand": "npm run dev",
"installCommand": "npm install",
"functions": {
"app/api/**/*.ts": {
"maxDuration": 30
}
},
"regions": ["iad1"],
"env": {
"DATABASE_URL": "@database-url"
}
}
Docker Deployment
# Dockerfile
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
Static Export
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
trailingSlash: true,
images: {
unoptimized: true
}
};
module.exports = nextConfig;
# Build and export
npm run build
# Serve static files
npx serve out
Pruebas
Biblioteca de Pruebas Jest y Reacting
# Install testing dependencies
npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
// jest.config.js
const nextJest = require('next/jest');
const createJestConfig = nextJest({
dir: './',
});
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapping: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/pages/(.*)$': '<rootDir>/pages/$1',
},
testEnvironment: 'jest-environment-jsdom',
};
module.exports = createJestConfig(customJestConfig);
// jest.setup.js
import '@testing-library/jest-dom';
Pruebas de componentes
// __tests__/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from '../components/Button';
describe('Button', () => {
it('renders button with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('applies correct variant styles', () => {
render(<Button variant="primary">Primary</Button>);
expect(screen.getByRole('button')).toHaveClass('btn-primary');
});
});
API Testing
// __tests__/api/users.test.ts
import { createMocks } from 'node-mocks-http';
import handler from '../../pages/api/users';
describe('/api/users', () => {
it('returns users list', async () => {
const { req, res } = createMocks({
method: 'GET',
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
const data = JSON.parse(res._getData());
expect(Array.isArray(data)).toBe(true);
});
it('creates new user', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'John Doe',
email: 'john@example.com',
},
});
await handler(req, res);
expect(res._getStatusCode()).toBe(201);
const data = JSON.parse(res._getData());
expect(data.name).toBe('John Doe');
});
});
E2E Pruebas con Playwright
# Install Playwright
npm install --save-dev @playwright/test
npx playwright install
// tests/e2e/home.spec.ts
import { test, expect } from '@playwright/test';
test('homepage loads correctly', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/My App/);
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});
test('navigation works', async ({ page }) => {
await page.goto('/');
await page.getByRole('link', { name: 'About' }).click();
await expect(page).toHaveURL('/about');
await expect(page.getByRole('heading', { name: 'About' })).toBeVisible();
});
Solución de problemas
Cuestiones comunes
Hidration Mismatch
// Fix hydration issues
import { useEffect, useState } from 'react';
export default function ClientOnlyComponent() {
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
setHasMounted(true);
}, []);
if (!hasMounted) {
return null;
}
return <div>Client-only content</div>;
}
Memory Leaks
// Proper cleanup
useEffect(() => {
const subscription = api.subscribe(data => {
setData(data);
});
return () => {
subscription.unsubscribe();
};
}, []);
```_
#### Errores de construcción
```bash
# Clear Next.js cache
rm -rf .next
# Clear node_modules
rm -rf node_modules package-lock.json
npm install
# Check for TypeScript errors
npx tsc --noEmit
Cuestiones de ejecución
// Optimize re-renders
import { memo, useMemo, useCallback } from 'react';
const ExpensiveComponent = memo(({ data, onUpdate }) => {
const processedData = useMemo(() => {
return data.map(item => ({
...item,
processed: true
}));
}, [data]);
const handleUpdate = useCallback((id) => {
onUpdate(id);
}, [onUpdate]);
return (
<div>
{processedData.map(item => (
<Item key={item.id} data={item} onUpdate={handleUpdate} />
))}
</div>
);
});
Buenas prácticas
Estructura del proyecto
# Recommended structure
src/
├── app/ # App Router pages
├── components/ # Reusable components
│ ├── ui/ # Basic UI components
│ └── features/ # Feature-specific components
├── lib/ # Utility functions
├── hooks/ # Custom hooks
├── types/ # TypeScript types
├── styles/ # Global styles
└── __tests__/ # Test files
Prácticas óptimas de rendimiento
// Use Server Components by default
export default async function Page() {
const data = await fetchData();
return <div>{data.title}</div>;
}
// Use Client Components only when needed
'use client';
export default function InteractiveComponent() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// Optimize images
<Image
src="/hero.jpg"
alt="Hero"
width={800}
height={600}
priority
placeholder="blur"
/>
SEO Buenas prácticas
// Metadata API
export const metadata = {
title: 'My Page',
description: 'Page description',
keywords: ['next.js', 'react', 'javascript'],
authors: [{ name: 'John Doe' }],
openGraph: {
title: 'My Page',
description: 'Page description',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
title: 'My Page',
description: 'Page description',
images: ['/twitter-image.jpg'],
},
};
// Dynamic metadata
export async function generateMetadata({ params }) {
const post = await fetchPost(params.id);
return {
title: post.title,
description: post.excerpt,
};
}
Prácticas óptimas de seguridad
// Environment variables
const apiKey = process.env.API_KEY; // Server-side only
const publicApiUrl = process.env.NEXT_PUBLIC_API_URL; // Client-side accessible
// Content Security Policy
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline';"
}
];
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
];
},
};
-...
Resumen
Next.js es un poderoso marco de reacción que proporciona una solución completa para la construcción de aplicaciones web modernas. Las características principales incluyen:
- App Router: Nuevo sistema de enrutamiento con diseños, estados de carga y límites de errores
- Server Components: Render componentes en el servidor para un mejor rendimiento
- ** Rutas de la API**: Puntos finales incorporados de la API con capacidades de personal completo
- ** Optimización de imagen**: Optimización de imagen automática y carga perezosa
- Performance: Optimizaciones incorporadas para los Vitales Web Core
- Deployment: Despliegue inigualable a Vercel y otras plataformas
Para obtener resultados óptimos, apalanque los componentes del servidor por defecto, utilice los componentes del cliente con moderación, implemente estrategias de caché adecuadas, y siga las convenciones Next.js para el enrutamiento y la búsqueda de datos.
" copia de la funciónToClipboard() {} comandos const = document.querySelectorAll('code'); que todos losCommands = '; comandos. paraCada(cmd = confianza allCommands += cmd.textContent + '\n'); navigator.clipboard.writeText(allCommands); alerta ('Todos los comandos copiados a portapapeles!'); }
función generaPDF() { ventana.print(); } ■/script título