--- name: nexthorizont description: "Stack de desarrollo para NextHorizont AI. Usar cuando se cree cualquier aplicación, componente, API, dashboard o feature con Next.js + Supabase + TypeScript. Incluye patrones de Server Components, Route Handlers, autenticación, y estructura de proyectos. Activar para: crear apps, scaffolding, APIs REST, dashboards, integración Supabase, componentes React, testing." --- # NextHorizont AI - Stack de Desarrollo ## Filosofía del Stack NextHorizont AI construye soluciones enterprise de IA. Todo el código debe ser: - **Production-ready**: No prototipos, código deployable desde el inicio - **Type-safe**: TypeScript estricto, NUNCA usar `any` - **Performant**: Server Components por defecto, mínimo JavaScript en cliente - **Mantenible**: Estructura clara, separación de responsabilidades --- ## Stack Tecnológico | Capa | Tecnología | Versión | |------|------------|---------| | Framework | Next.js (App Router) | 15+ | | Base de datos | Supabase (PostgreSQL) | Latest | | Auth | Supabase Auth / Clerk | Latest | | Styling | Tailwind CSS | 4+ | | UI Components | shadcn/ui | Latest | | Estado cliente | Zustand / React Query | Latest | | Validación | Zod | Latest | | Deploy | Vercel | - | --- ## Patrón de Decisión: Cómo Acceder a Datos ``` ┌─────────────────────────────────────────────────────────────────┐ │ ¿CÓMO ACCEDO A DATOS? │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ ¿Es lectura para renderizar? │ └───────────────────────────────┘ │ │ SÍ NO │ │ ▼ ▼ ┌─────────────────┐ ┌──────────────────────┐ │ SERVER COMPONENT│ │ ¿Es mutación simple │ │ + acceso directo│ │ con UX acoplada? │ │ a Supabase │ └──────────────────────┘ │ (PATRÓN DEFAULT)│ │ │ └─────────────────┘ SÍ NO │ │ ▼ ▼ ┌──────────────┐ ┌─────────────────┐ │SERVER ACTION │ │ ROUTE HANDLER │ │"use server" │ │ API REST │ │Forms simples │ │ (RECOMENDADO) │ └──────────────┘ └─────────────────┘ ``` ### 1. Server Components + Acceso Directo (DEFAULT - 80% de casos) ```typescript // app/dashboard/page.tsx import { createClient } from '@/lib/supabase/server' export default async function DashboardPage() { const supabase = await createClient() const { data: projects, error } = await supabase .from('projects') .select('id, name, status, created_at') .order('created_at', { ascending: false }) if (error) throw new Error(error.message) return (
{projects.map(project => ( ))}
) } ``` ### 2. Route Handlers (APIs reutilizables, testing, seguridad) ```typescript // app/api/projects/route.ts import { createClient } from '@/lib/supabase/server' import { NextResponse } from 'next/server' import { z } from 'zod' const CreateProjectSchema = z.object({ name: z.string().min(1).max(100), description: z.string().optional(), }) export async function GET() { const supabase = await createClient() const { data, error } = await supabase .from('projects') .select('*') .order('created_at', { ascending: false }) if (error) { return NextResponse.json({ error: error.message }, { status: 500 }) } return NextResponse.json(data) } export async function POST(request: Request) { const supabase = await createClient() const body = await request.json() const parsed = CreateProjectSchema.safeParse(body) if (!parsed.success) { return NextResponse.json( { error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 } ) } const { data, error } = await supabase .from('projects') .insert(parsed.data) .select() .single() if (error) { return NextResponse.json({ error: error.message }, { status: 500 }) } return NextResponse.json(data, { status: 201 }) } ``` ### 3. Server Actions (SOLO para mutaciones simples con UX acoplada) ```typescript // app/actions/project.ts 'use server' import { createClient } from '@/lib/supabase/server' import { revalidatePath } from 'next/cache' import { z } from 'zod' const schema = z.object({ name: z.string().min(1), }) export async function createProject(formData: FormData) { const supabase = await createClient() const parsed = schema.safeParse({ name: formData.get('name'), }) if (!parsed.success) { return { error: 'Invalid data' } } const { error } = await supabase .from('projects') .insert(parsed.data) if (error) { return { error: error.message } } revalidatePath('/dashboard') return { success: true } } ``` --- ## Estructura de Proyecto ``` nexthorizont-app/ ├── app/ │ ├── (auth)/ │ │ ├── login/page.tsx │ │ └── register/page.tsx │ ├── (dashboard)/ │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── projects/ │ │ ├── page.tsx │ │ ├── [id]/page.tsx │ │ └── new/page.tsx │ ├── api/ │ │ ├── projects/ │ │ │ ├── route.ts │ │ │ └── [id]/route.ts │ │ └── webhooks/ │ │ └── stripe/route.ts │ ├── layout.tsx │ └── page.tsx ├── components/ │ ├── ui/ # shadcn/ui components │ ├── forms/ # Form components │ ├── layouts/ # Layout components │ └── features/ # Feature-specific components ├── lib/ │ ├── supabase/ │ │ ├── client.ts # Browser client │ │ ├── server.ts # Server client │ │ ├── middleware.ts # Auth middleware │ │ └── types.ts # Generated types │ ├── utils/ │ │ ├── cn.ts # classnames helper │ │ └── format.ts # Formatting utilities │ └── validations/ │ └── schemas.ts # Zod schemas ├── hooks/ │ └── use-projects.ts # Custom hooks ├── types/ │ ├── database.ts # Supabase generated types │ └── index.ts # App types ├── middleware.ts # Next.js middleware └── supabase/ ├── migrations/ # SQL migrations └── seed.sql # Seed data ``` --- ## Configuración Supabase ### Cliente del Servidor ```typescript // lib/supabase/server.ts import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' import type { Database } from '@/types/database' export async function createClient() { const cookieStore = await cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll() }, setAll(cookiesToSet) { try { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options) ) } catch { // Server Component - ignore } }, }, } ) } ``` ### Cliente del Browser ```typescript // lib/supabase/client.ts import { createBrowserClient } from '@supabase/ssr' import type { Database } from '@/types/database' export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) } ``` ### Middleware de Auth ```typescript // middleware.ts import { createServerClient } from '@supabase/ssr' import { NextResponse, type NextRequest } from 'next/server' export async function middleware(request: NextRequest) { let supabaseResponse = NextResponse.next({ request }) const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return request.cookies.getAll() }, setAll(cookiesToSet) { cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value) ) supabaseResponse = NextResponse.next({ request }) cookiesToSet.forEach(({ name, value, options }) => supabaseResponse.cookies.set(name, value, options) ) }, }, } ) const { data: { user } } = await supabase.auth.getUser() // Rutas protegidas if (!user && request.nextUrl.pathname.startsWith('/dashboard')) { const url = request.nextUrl.clone() url.pathname = '/login' return NextResponse.redirect(url) } return supabaseResponse } export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', ], } ``` --- ## TypeScript Estricto ### Reglas NO NEGOCIABLES ```typescript // ❌ PROHIBIDO - NUNCA usar any const data: any = await fetch('/api/data') function process(input: any): any { } // ✅ CORRECTO - Tipos específicos siempre interface Project { id: string name: string status: 'active' | 'archived' | 'draft' createdAt: Date } const data: Project[] = await fetch('/api/data').then(r => r.json()) function process>(input: T): T { return input } ``` ### tsconfig.json Recomendado ```json { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "noImplicitAny": true, "strictNullChecks": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "exactOptionalPropertyTypes": true } } ``` --- ## Componentes UI con shadcn/ui ### Instalación Base ```bash npx shadcn@latest init npx shadcn@latest add button card form input table dialog ``` ### Patrón de Componente ```typescript // components/features/project-card.tsx import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import type { Project } from '@/types' interface ProjectCardProps { project: Project onSelect?: (id: string) => void } export function ProjectCard({ project, onSelect }: ProjectCardProps) { const statusColors = { active: 'bg-green-500', archived: 'bg-gray-500', draft: 'bg-yellow-500', } as const return ( onSelect?.(project.id)} >
{project.name} {project.status}

Created {project.createdAt.toLocaleDateString()}

) } ``` --- ## Manejo de Errores ### Error Boundaries ```typescript // app/dashboard/error.tsx 'use client' import { useEffect } from 'react' import { Button } from '@/components/ui/button' export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { useEffect(() => { console.error(error) }, [error]) return (

Algo salió mal

{error.message}

) } ``` ### Loading States ```typescript // app/dashboard/loading.tsx import { Skeleton } from '@/components/ui/skeleton' export default function Loading() { return (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) } ``` --- ## Validación con Zod ```typescript // lib/validations/schemas.ts import { z } from 'zod' export const ProjectSchema = z.object({ name: z.string() .min(1, 'El nombre es requerido') .max(100, 'Máximo 100 caracteres'), description: z.string().max(500).optional(), status: z.enum(['active', 'archived', 'draft']).default('draft'), }) export const PaginationSchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().min(1).max(100).default(20), }) export type Project = z.infer export type Pagination = z.infer ``` --- ## Testing ### Vitest Config ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import path from 'path' export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', setupFiles: ['./tests/setup.ts'], include: ['**/*.test.{ts,tsx}'], }, resolve: { alias: { '@': path.resolve(__dirname, './'), }, }, }) ``` ### Test de Componente ```typescript // components/features/__tests__/project-card.test.tsx import { render, screen, fireEvent } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import { ProjectCard } from '../project-card' describe('ProjectCard', () => { const mockProject = { id: '1', name: 'Test Project', status: 'active' as const, createdAt: new Date('2025-01-01'), } it('renders project name', () => { render() expect(screen.getByText('Test Project')).toBeInTheDocument() }) it('calls onSelect when clicked', () => { const onSelect = vi.fn() render() fireEvent.click(screen.getByRole('article')) expect(onSelect).toHaveBeenCalledWith('1') }) }) ``` --- ## Checklist Pre-Deploy - [ ] TypeScript: `npx tsc --noEmit` sin errores - [ ] Lint: `npx eslint . --ext .ts,.tsx` sin errores - [ ] Tests: `npm run test` pasando - [ ] Build: `npm run build` exitoso - [ ] Variables de entorno configuradas en Vercel - [ ] RLS policies activas en Supabase - [ ] Migrations aplicadas en producción --- ## DO NOT - ❌ Usar `any` - siempre tipos específicos - ❌ Fetch en Client Components para datos iniciales - usar Server Components - ❌ `getServerSideProps` o `getStaticProps` - son de Pages Router - ❌ Guardar secretos en código - usar variables de entorno - ❌ Desactivar RLS en Supabase - siempre políticas activas - ❌ Console.log en producción - usar logger estructurado - ❌ Exponer errores de DB al cliente - mensajes genéricos