Next.js es un framework de React para producción que permite renderizado híbrido estático y SSR (Server-Side Rendering). Combinado con TypeScript, ofrece una experiencia de desarrollo robusta y escalable.
# Crear nuevo proyecto con TypeScript
npx create-next-app@latest --typescript
# o
yarn create next-app --typescript
# Estructura básica del proyecto
.
├── pages/
│ ├── _app.tsx # Componente principal
│ ├── _document.tsx # Personalización del documento HTML
│ ├── api/ # API routes
│ └── index.tsx # Página principal
├── public/ # Archivos estáticos
├── styles/ # CSS/SCSS
├── tsconfig.json # Configuración de TypeScript
└── next.config.js # Configuración de Next.js
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
/// <reference types="next" />
/// <reference types="next/types/global" />
// Tipos para imágenes estáticas
declare module "*.png" {
const value: string;
export default value;
}
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
images: {
domains: ['example.com'],
},
// Configuración de TypeScript
typescript: {
ignoreBuildErrors: false, // No ignorar errores en build
tsconfigPath: './tsconfig.json', // Ruta personalizada
},
}
module.exports = nextConfig
// pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
const Home: NextPage = () => {
return (
<div>
<Head>
<title>Mi App</title>
</Head>
<h1>Bienvenido</h1>
</div>
)
}
export default Home
import type { NextPage } from 'next'
// Página sin props
const Page: NextPage = () => { /* ... */ }
// Página con props
type HomeProps = {
posts: Post[]
}
const Home: NextPage<HomeProps> = ({ posts }) => {
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
// pages/blog/[slug].tsx
import type {
GetStaticProps,
GetStaticPaths,
NextPage
} from 'next'
type Post = {
slug: string
title: string
content: string
}
type Props = {
post: Post
}
const BlogPost: NextPage<Props> = ({ post }) => {
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
export const getStaticProps: GetStaticProps<Props> = async (context) => {
const { slug } = context.params as { slug: string }
// Fetch post data
return { props: { post } }
}
export const getStaticPaths: GetStaticPaths = async () => {
// Obtener slugs de posts
return {
paths: posts.map(post => ({ params: { slug: post.slug } })),
fallback: false
}
}
import type { GetStaticProps, NextPage } from 'next'
type Product = {
id: number
name: string
price: number
}
type HomeProps = {
products: Product[]
}
export const getStaticProps: GetStaticProps<HomeProps> = async () => {
const res = await fetch('https://api.example.com/products')
const products: Product[] = await res.json()
return {
props: { products },
revalidate: 60 // ISR: Regenerar cada 60 segundos
}
}
const Home: NextPage<HomeProps> = ({ products }) => {
// Renderizar productos
}
import type { GetServerSideProps, NextPage } from 'next'
type User = {
id: number
name: string
email: string
}
type ProfileProps = {
user: User
}
export const getServerSideProps: GetServerSideProps<ProfileProps> = async (context) => {
const { req, res, params } = context
const { userId } = params as { userId: string }
// Autenticación basada en cookies
const user = await getUserFromRequest(req)
if (!user) {
return {
redirect: {
destination: '/login',
permanent: false
}
}
}
return { props: { user } }
}
// pages/api/users/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next'
type User = {
id: number
name: string
email: string
}
type ErrorResponse = {
error: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<User | ErrorResponse>
) {
const { id } = req.query
if (req.method === 'GET') {
const user = getUserById(Number(id))
if (!user) {
return res.status(404).json({ error: 'User not found' })
}
return res.status(200).json(user)
}
res.setHeader('Allow', ['GET'])
res.status(405).json({ error: `Method ${req.method} not allowed` })
}
// components/Button.tsx
import { ReactNode } from 'react'
type ButtonProps = {
children: ReactNode
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
onClick?: () => void
disabled?: boolean
className?: string
}
export const Button = ({
children,
variant = 'primary',
size = 'md',
...props
}: ButtonProps) => {
const baseClasses = 'rounded font-medium transition-colors'
const variantClasses = {
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
danger: 'bg-red-600 hover:bg-red-700 text-white'
}
const sizeClasses = {
sm: 'py-1 px-3 text-sm',
md: 'py-2 px-4 text-base',
lg: 'py-3 px-6 text-lg'
}
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
{...props}
>
{children}
</button>
)
}
// context/AuthContext.tsx
import { createContext, useContext, ReactNode, useState } from 'react'
type User = {
id: string
name: string
email: string
}
type AuthContextType = {
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
isAuthenticated: boolean
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null)
const login = async (email: string, password: string) => {
// Lógica de autenticación
const authenticatedUser = await authenticate(email, password)
setUser(authenticatedUser)
}
const logout = () => {
setUser(null)
}
const value = {
user,
login,
logout,
isAuthenticated: !!user
}
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export const useAuth = () => {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
// hooks/useFetch.ts
import { useState, useEffect } from 'react'
type FetchState<T> = {
data: T | null
loading: boolean
error: Error | null
}
export function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null
})
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(response.statusText)
}
const data = (await response.json()) as T
setState({ data, loading: false, error: null })
} catch (error) {
setState({ data: null, loading: false, error: error as Error })
}
}
fetchData()
}, [url])
return state
}
// Uso en un componente
const { data, loading, error } = useFetch<Product[]>('/api/products')
// hooks/useLocalStorage.ts
import { useState, useEffect } from 'react'
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
if (typeof window !== 'undefined') {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
}
return initialValue
} catch (error) {
console.error(error)
return initialValue
}
})
const setValue = (value: T) => {
try {
setStoredValue(value)
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(value))
}
} catch (error) {
console.error(error)
}
}
return [storedValue, setValue]
}
// Ejemplo de uso
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')
// components/Container.tsx
import styled from 'styled-components'
type ContainerProps = {
fluid?: boolean
maxWidth?: string
}
const StyledContainer = styled.div<ContainerProps>`
width: 100%;
margin: 0 auto;
padding: 0 1rem;
max-width: ${props => props.fluid ? '100%' : props.maxWidth || '1200px'};
`
export const Container: React.FC<ContainerProps> = ({
children,
...props
}) => {
return <StyledContainer {...props}>{children}</StyledContainer>
}
// components/Card.module.css
.card {
border: 1px solid #eaeaea;
border-radius: 8px;
padding: 1.5rem;
transition: box-shadow 0.2s ease;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
// components/Card.tsx
import styles from './Card.module.css'
type CardProps = {
children: React.ReactNode
className?: string
}
export const Card = ({ children, className }: CardProps) => {
return (
<div className={`${styles.card} ${className || ''}`}>
{children}
</div>
)
}
// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}'
],
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#2563eb',
light: '#3b82f6'
}
}
}
}
}
// Ejemplo de componente con Tailwind
type AlertProps = {
variant: 'success' | 'error' | 'warning'
message: string
}
export const Alert = ({ variant, message }: AlertProps) => {
const colors = {
success: 'bg-green-100 text-green-800',
error: 'bg-red-100 text-red-800',
warning: 'bg-yellow-100 text-yellow-800'
}
return (
<div className={`p-4 rounded ${colors[variant]}`}>
{message}
</div>
)
}
// components/__tests__/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from '../Button'
describe('Button', () => {
it('renders correctly with default props', () => {
render(<Button>Click me</Button>)
const button = screen.getByRole('button', { name: /click me/i })
expect(button).toHaveClass('bg-blue-600')
})
it('calls onClick handler when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByText(/click me/i))
expect(handleClick).toHaveBeenCalledTimes(1)
})
})
// cypress/component/LoginForm.cy.ts
import LoginForm from '../../components/LoginForm'
describe('LoginForm', () => {
it('should submit the form with valid data', () => {
const onSubmit = cy.stub().as('onSubmit')
cy.mount(<LoginForm onSubmit={onSubmit} />)
cy.get('input[name="email"]').type('user@example.com')
cy.get('input[name="password"]').type('password123')
cy.get('button[type="submit"]').click()
cy.get('@onSubmit').should('have.been.calledWith', {
email: 'user@example.com',
password: 'password123'
})
})
})
// pages/_app.tsx
import { Analytics } from '@vercel/analytics/react'
export default function App({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
<Analytics />
</>
)
}
import Image from 'next/image'
const OptimizedImage = () => (
<Image
src="/profile.jpg"
alt="Profile picture"
width={500}
height={500}
priority // Precarga para imágenes importantes
quality={80} // Calidad reducida para mejor rendimiento
placeholder="blur" // Efecto de carga progresiva
blurDataURL="data:image/png;base64,..." // Miniatura en base64
/>
)
Next.js con TypeScript ofrece una combinación poderosa para construir aplicaciones React modernas, escalables y fáciles de mantener. El sistema de tipos mejora la calidad del código y la experiencia de desarrollo.