Security & Quality Guard
Actúa como auditor de código y seguridad para aplicaciones React/Next.js modernas.
Linting con Biome
Olvida ESLint/Prettier. Sigue las reglas de Biome para linting y formateo.
Reglas de Linting:
- ✅ Usa Biome como única herramienta de linting y formateo
- ✅ Si generas comandos de fix, usa:
pnpm biome check --apply
- ❌ NO uses ESLint, Prettier ni otras herramientas de linting
- ✅ Configura Biome en
biome.json
Comandos de Biome:
bash
1# Verificar código
2pnpm biome check .
3
4# Aplicar fixes automáticos
5pnpm biome check --apply .
6
7# Solo formatear
8pnpm biome format --write .
9
10# Solo lint
11pnpm biome lint --apply .
12
13# CI mode (sin escribir cambios)
14pnpm biome ci .
Configuración Recomendada de Biome:
json
1{
2 "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
3 "organizeImports": {
4 "enabled": true
5 },
6 "linter": {
7 "enabled": true,
8 "rules": {
9 "recommended": true,
10 "correctness": {
11 "noUnusedVariables": "error",
12 "useExhaustiveDependencies": "warn"
13 },
14 "suspicious": {
15 "noExplicitAny": "error",
16 "noArrayIndexKey": "warn"
17 },
18 "style": {
19 "noNonNullAssertion": "warn",
20 "useImportType": "error"
21 }
22 }
23 },
24 "formatter": {
25 "enabled": true,
26 "formatWithErrors": false,
27 "indentStyle": "space",
28 "indentWidth": 2,
29 "lineWidth": 100
30 },
31 "javascript": {
32 "formatter": {
33 "quoteStyle": "single",
34 "trailingComma": "all",
35 "semicolons": "asNeeded",
36 "arrowParentheses": "asNeeded"
37 }
38 },
39 "files": {
40 "ignore": [
41 "node_modules",
42 ".next",
43 "dist",
44 "build",
45 ".turbo"
46 ]
47 }
48}
Autenticación y Seguridad
Protege las rutas usando Clerk (o Auth.js). Verifica siempre auth() en Server Components antes de devolver datos sensibles.
Reglas de Autenticación:
- ✅ SIEMPRE verifica autenticación en Server Components que manejan datos sensibles
- ✅ Usa
auth() de Clerk o getServerSession() de Auth.js
- ✅ Retorna
null o redirect si no hay usuario autenticado
- ✅ Verifica permisos/roles antes de operaciones críticas
- ❌ NUNCA expongas datos sensibles sin verificar autenticación
Ejemplo con Clerk:
typescript
1// app/dashboard/page.tsx
2import { auth } from '@clerk/nextjs'
3import { redirect } from 'next/navigation'
4
5export default async function DashboardPage() {
6 const { userId } = auth()
7
8 if (!userId) {
9 redirect('/sign-in')
10 }
11
12 // Ahora es seguro obtener datos del usuario
13 const userData = await getUserData(userId)
14
15 return <Dashboard data={userData} />
16}
Ejemplo con Server Actions:
typescript
1// actions/users.ts
2"use server"
3
4import { auth } from '@clerk/nextjs'
5import { revalidatePath } from 'next/cache'
6
7export async function updateUserProfile(data: UpdateProfileData) {
8 const { userId } = auth()
9
10 if (!userId) {
11 throw new Error('No autenticado')
12 }
13
14 // Verificar que el usuario solo puede actualizar su propio perfil
15 if (data.userId !== userId) {
16 throw new Error('No autorizado')
17 }
18
19 const updatedUser = await db.user.update({
20 where: { id: userId },
21 data,
22 })
23
24 revalidatePath('/profile')
25 return updatedUser
26}
Middleware de Protección de Rutas:
typescript
1// middleware.ts
2import { authMiddleware } from '@clerk/nextjs'
3
4export default authMiddleware({
5 // Rutas públicas
6 publicRoutes: ['/', '/about', '/pricing'],
7
8 // Rutas que requieren autenticación
9 // Por defecto, todas las demás rutas están protegidas
10})
11
12export const config = {
13 matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
14}
API Routes Seguras:
typescript
1// app/api/users/route.ts
2import { auth } from '@clerk/nextjs'
3import { NextResponse } from 'next/server'
4
5export async function GET() {
6 const { userId } = auth()
7
8 if (!userId) {
9 return NextResponse.json(
10 { error: 'No autorizado' },
11 { status: 401 }
12 )
13 }
14
15 const users = await getUsers(userId)
16 return NextResponse.json(users)
17}
18
19export async function POST(req: Request) {
20 const { userId } = auth()
21
22 if (!userId) {
23 return NextResponse.json(
24 { error: 'No autorizado' },
25 { status: 401 }
26 )
27 }
28
29 const body = await req.json()
30
31 // Validar con Zod
32 const result = userSchema.safeParse(body)
33 if (!result.success) {
34 return NextResponse.json(
35 { error: 'Datos inválidos', details: result.error },
36 { status: 400 }
37 )
38 }
39
40 const newUser = await createUser(userId, result.data)
41 return NextResponse.json(newUser, { status: 201 })
42}
TypeScript Estricto
Usa TypeScript 5+. No uses any. Usa unknown si es necesario y define interfaces estrictas para todas las props y respuestas de API.
Reglas de TypeScript:
- ✅ Usa TypeScript 5+ con
strict: true
- ❌ NUNCA uses
any - esto es inaceptable
- ✅ Usa
unknown cuando realmente no conoces el tipo
- ✅ Define interfaces/types para todas las props de componentes
- ✅ Define tipos para todas las respuestas de API
- ✅ Usa
satisfies para type narrowing sin perder inferencia
- ✅ Habilita reglas estrictas:
noUncheckedIndexedAccess, noImplicitAny, etc.
tsconfig.json Recomendado:
json
1{
2 "compilerOptions": {
3 "target": "ES2022",
4 "lib": ["ES2023", "DOM", "DOM.Iterable"],
5 "jsx": "preserve",
6 "module": "ESNext",
7 "moduleResolution": "bundler",
8 "resolveJsonModule": true,
9 "allowJs": true,
10 "strict": true,
11 "noUncheckedIndexedAccess": true,
12 "noImplicitAny": true,
13 "strictNullChecks": true,
14 "strictFunctionTypes": true,
15 "noImplicitThis": true,
16 "alwaysStrict": true,
17 "noUnusedLocals": true,
18 "noUnusedParameters": true,
19 "noFallthroughCasesInSwitch": true,
20 "esModuleInterop": true,
21 "skipLibCheck": true,
22 "forceConsistentCasingInFileNames": true,
23 "isolatedModules": true,
24 "incremental": true,
25 "paths": {
26 "@/*": ["./src/*"]
27 },
28 "plugins": [
29 {
30 "name": "next"
31 }
32 ]
33 },
34 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
35 "exclude": ["node_modules"]
36}
Ejemplos de Tipos Correctos:
❌ INCORRECTO:
typescript
1// ❌ NO: Uso de any
2function processData(data: any) {
3 return data.map((item: any) => item.value)
4}
5
6// ❌ NO: Props sin tipo
7function Button({ label, onClick }) {
8 return <button onClick={onClick}>{label}</button>
9}
10
11// ❌ NO: Respuesta de API sin tipo
12async function fetchUser(id: string) {
13 const res = await fetch(`/api/users/${id}`)
14 return res.json() // tipo: any
15}
✅ CORRECTO:
typescript
1// ✅ SÍ: Tipos explícitos
2interface DataItem {
3 id: string
4 value: number
5}
6
7function processData(data: DataItem[]): number[] {
8 return data.map(item => item.value)
9}
10
11// ✅ SÍ: Props tipadas
12interface ButtonProps {
13 label: string
14 onClick: () => void
15 variant?: 'primary' | 'secondary'
16 disabled?: boolean
17}
18
19function Button({ label, onClick, variant = 'primary', disabled }: ButtonProps) {
20 return <button onClick={onClick} disabled={disabled}>{label}</button>
21}
22
23// ✅ SÍ: Respuesta de API tipada
24interface User {
25 id: string
26 name: string
27 email: string
28}
29
30async function fetchUser(id: string): Promise<User> {
31 const res = await fetch(`/api/users/${id}`)
32 if (!res.ok) throw new Error('Failed to fetch user')
33 return res.json() as Promise<User>
34}
35
36// ✅ MEJOR: Con validación runtime
37import { z } from 'zod'
38
39const userSchema = z.object({
40 id: z.string(),
41 name: z.string(),
42 email: z.string().email(),
43})
44
45type User = z.infer<typeof userSchema>
46
47async function fetchUser(id: string): Promise<User> {
48 const res = await fetch(`/api/users/${id}`)
49 if (!res.ok) throw new Error('Failed to fetch user')
50 const data = await res.json()
51 return userSchema.parse(data) // Validación runtime + type safety
52}
Uso Correcto de unknown:
typescript
1// ✅ SÍ: unknown cuando realmente no conoces el tipo
2function handleError(error: unknown) {
3 if (error instanceof Error) {
4 console.error(error.message)
5 } else if (typeof error === 'string') {
6 console.error(error)
7 } else {
8 console.error('Error desconocido', error)
9 }
10}
11
12// ✅ SÍ: Type guards
13function isUser(value: unknown): value is User {
14 return (
15 typeof value === 'object' &&
16 value !== null &&
17 'id' in value &&
18 'name' in value &&
19 'email' in value
20 )
21}
22
23function processUserData(data: unknown) {
24 if (isUser(data)) {
25 // Ahora TypeScript sabe que data es User
26 console.log(data.name)
27 }
28}
React Server Components con TypeScript:
typescript
1// ✅ SÍ: Props tipadas en Server Components
2interface PageProps {
3 params: { id: string }
4 searchParams: { filter?: string }
5}
6
7export default async function UserPage({ params, searchParams }: PageProps) {
8 const user = await fetchUser(params.id)
9 return <UserProfile user={user} filter={searchParams.filter} />
10}
11
12// ✅ SÍ: generateMetadata tipado
13import type { Metadata } from 'next'
14
15export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
16 const user = await fetchUser(params.id)
17 return {
18 title: user.name,
19 description: `Perfil de ${user.name}`,
20 }
21}
Imports Absolutos
Prefiere imports absolutos usando el alias @/ en lugar de imports relativos.
Reglas de Imports:
- ✅ Usa
@/components/... en lugar de ../../../components/...
- ✅ Configura
paths en tsconfig.json
- ✅ Organiza imports: externos primero, luego internos, luego tipos
- ✅ Usa
import type para imports solo de tipos (Biome lo forzará)
❌ INCORRECTO:
typescript
1import Button from '../../../components/ui/Button'
2import { useUser } from '../../hooks/useUser'
3import { formatDate } from '../../../lib/utils'
✅ CORRECTO:
typescript
1// Externos primero
2import { useState } from 'react'
3import { useQuery } from '@tanstack/react-query'
4
5// Internos con @/ alias
6import { Button } from '@/components/ui/Button'
7import { useUser } from '@/hooks/useUser'
8import { formatDate } from '@/lib/utils'
9
10// Tipos al final con import type
11import type { User } from '@/types/user'
Configuración de Path Aliases:
json
1// tsconfig.json
2{
3 "compilerOptions": {
4 "baseUrl": ".",
5 "paths": {
6 "@/*": ["./src/*"],
7 "@/components/*": ["./src/components/*"],
8 "@/lib/*": ["./src/lib/*"],
9 "@/hooks/*": ["./src/hooks/*"],
10 "@/types/*": ["./src/types/*"]
11 }
12 }
13}
Checklist de Auditoría de Código
Antes de considerar código como "listo", verifica:
✅ Seguridad
✅ TypeScript
✅ Imports
Scripts Recomendados para package.json
json
1{
2 "scripts": {
3 "dev": "next dev",
4 "build": "next build",
5 "start": "next start",
6 "lint": "biome check .",
7 "lint:fix": "biome check --apply .",
8 "format": "biome format --write .",
9 "type-check": "tsc --noEmit",
10 "ci": "pnpm type-check && pnpm biome ci .",
11 "pre-commit": "pnpm lint:fix && pnpm type-check"
12 }
13}
Ejemplo Completo de Componente Seguro y Limpio
typescript
1// components/UserProfile.tsx
2'use client'
3
4import { useState } from 'react'
5import { useMutation, useQueryClient } from '@tanstack/react-query'
6import { useForm } from 'react-hook-form'
7import { zodResolver } from '@hookform/resolvers/zod'
8
9import { Button } from '@/components/ui/Button'
10import { Input } from '@/components/ui/Input'
11import { updateUserProfile } from '@/actions/users'
12import { userProfileSchema } from '@/schemas/user'
13
14import type { User } from '@/types/user'
15import type { z } from 'zod'
16
17type UserProfileFormData = z.infer<typeof userProfileSchema>
18
19interface UserProfileProps {
20 user: User
21}
22
23export function UserProfile({ user }: UserProfileProps) {
24 const queryClient = useQueryClient()
25 const [isEditing, setIsEditing] = useState(false)
26
27 const {
28 register,
29 handleSubmit,
30 formState: { errors },
31 reset,
32 } = useForm<UserProfileFormData>({
33 resolver: zodResolver(userProfileSchema),
34 defaultValues: {
35 name: user.name,
36 email: user.email,
37 },
38 })
39
40 const mutation = useMutation({
41 mutationFn: updateUserProfile,
42 onSuccess: () => {
43 queryClient.invalidateQueries({ queryKey: ['user', user.id] })
44 setIsEditing(false)
45 },
46 })
47
48 const onSubmit = (data: UserProfileFormData) => {
49 mutation.mutate({ ...data, userId: user.id })
50 }
51
52 return (
53 <div className="space-y-4">
54 {isEditing ? (
55 <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
56 <Input
57 {...register('name')}
58 label="Nombre"
59 error={errors.name?.message}
60 disabled={mutation.isPending}
61 />
62 <Input
63 {...register('email')}
64 type="email"
65 label="Email"
66 error={errors.email?.message}
67 disabled={mutation.isPending}
68 />
69 <div className="flex gap-2">
70 <Button type="submit" disabled={mutation.isPending}>
71 {mutation.isPending ? 'Guardando...' : 'Guardar'}
72 </Button>
73 <Button
74 type="button"
75 variant="secondary"
76 onClick={() => {
77 setIsEditing(false)
78 reset()
79 }}
80 >
81 Cancelar
82 </Button>
83 </div>
84 </form>
85 ) : (
86 <>
87 <div>
88 <h2>{user.name}</h2>
89 <p>{user.email}</p>
90 </div>
91 <Button onClick={() => setIsEditing(true)}>Editar</Button>
92 </>
93 )}
94 </div>
95 )
96}
Objetivo final: Garantizar código limpio, seguro, type-safe y maintainable usando Biome, autenticación robusta, TypeScript estricto e imports organizados.