Skip to main content

App Router Overview

ICL Cotizaciones uses Next.js 16 App Router with file-system based routing, server components by default, and co-located API routes.
The application uses route groups, dynamic routes, and layout-based authentication without implementing Next.js middleware.

Route Tree

src/app/
├── layout.tsx                          ← Root layout (HTML, Work Sans font)
├── page.tsx                            ← Redirect: /cotizaciones if logged in, /login if not

├── login/
│   └── page.tsx                        ← Login form (client component)

└── (dashboard)/                        ← Route group (no URL segment)
    ├── layout.tsx                      ← Protected layout: session guard + Header + Sidebar

    ├── cotizaciones/
    │   ├── page.tsx                    ← Quotations list with filters
    │   ├── nueva/
    │   │   └── page.tsx                ← New quotation form
    │   └── [id]/
    │       └── page.tsx                ← View/edit quotation (dynamic route)

    ├── dashboard/
    │   ├── general/
    │   │   └── page.tsx                ← General dashboard (charts)
    │   ├── por-cliente/
    │   │   └── page.tsx                ← Dashboard grouped by client
    │   ├── filtradas/
    │   │   └── page.tsx                ← Dashboard with advanced filters
    │   ├── por-vendedor/
    │   │   └── page.tsx                ← Dashboard grouped by sales rep
    │   └── contactos/
    │       └── page.tsx                ← Contacts dashboard

    └── maestros/
        ├── clientes/
        │   └── page.tsx                ← Clients CRUD (admin via API; sidebar hidden for non-admin)
        ├── origenes/
        │   └── page.tsx                ← Origins CRUD
        ├── vias/
        │   └── page.tsx                ← Routes CRUD
        ├── acuerdos/
        │   └── page.tsx                ← Commercial agreements CRUD
        ├── usuarios/
        │   └── page.tsx                ← Users CRUD
        ├── locaciones/
        │   └── page.tsx                ← Locations CRUD (origins + routes)
        ├── pricing-netos/
        │   └── page.tsx                ← Net pricing table (unified FCL + LCL)
        └── tarifario-puertos-base/
            └── page.tsx                ← Base port rates

Route Table

Public Routes

URLPurposeLayoutAuth RequiredData Source
/Smart redirectRootNo (reads session)getSession() server-side
/loginLogin formRootNoPOST /api/auth/login

Protected Routes (Dashboard)

All routes under (dashboard)/ are protected by the layout session guard.

Quotations

URLPurposeData APIs
/cotizacionesPaginated quotations listGET /api/cotizaciones
/cotizaciones/nuevaCreate new quotationGET /api/cotizaciones/next-nro
GET /api/clientes
GET /api/origenes
GET /api/vias
GET /api/usuarios
GET /api/auth/me
GET /api/acuerdos
GET /api/tarifario-puertos-base
/cotizaciones/[id]View/edit quotationGET /api/cotizaciones/[id]
Same APIs as /nueva

Dashboards

URLPurposeData APIs
/dashboard/generalGeneral chartsGET /api/dashboard?tipo=general
/dashboard/por-clienteClient metricsGET /api/dashboard?tipo=por-cliente
/dashboard/filtradasFiltered dashboardGET /api/dashboard?tipo=filtradas
/dashboard/por-vendedorSales rep metricsGET /api/dashboard?tipo=por-vendedor
/dashboard/contactosContacts dashboardGET /api/dashboard?tipo=contactos

Master Data (Admin Only)

The sidebar shows master data sections only for admin roles (DIRECTOR, GERENTE, ADMINISTRACION). API endpoints enforce authorization with isAdmin() checks.
URLPurposeData APIs
/maestros/clientesClients CRUDGET/POST /api/clientes
PUT/DELETE /api/clientes/[id]
/maestros/origenesOrigins CRUDGET/POST /api/origenes
PUT/DELETE /api/origenes/[id]
/maestros/viasRoutes CRUDGET/POST /api/vias
PUT/DELETE /api/vias/[id]
/maestros/acuerdosCommercial agreements CRUDGET/POST /api/acuerdos
GET/PUT/DELETE /api/acuerdos/[id]
/maestros/usuariosUsers CRUDGET/POST /api/usuarios
PUT/DELETE /api/usuarios/[id]
/maestros/locacionesLocations CRUDGET/POST /api/origenes
GET/POST /api/vias
Plus respective [id] routes
/maestros/pricing-netosNet pricing tableGET/POST /api/pricing-netos
PUT/DELETE /api/pricing-netos/[id]
/maestros/tarifario-puertos-baseBase port ratesGET/POST /api/tarifario-puertos-base
PUT/DELETE /api/tarifario-puertos-base/[id]
Master data pages are client components without built-in guards. Effective protection is enforced at the API level. Non-admin users who manually navigate to /maestros/* cannot perform mutations.

API Routes

Authentication

EndpointMethodsAccess ControlPurpose
/api/auth/loginPOSTPublicValidate credentials, create session
/api/auth/logoutPOSTAuthenticatedDestroy session
/api/auth/meGETAuthenticatedGet current user data

Quotations

EndpointMethodsAccess ControlPurpose
/api/cotizacionesGETFiltered by user_id for non-adminList quotations
/api/cotizacionesPOSTAny authenticated userCreate quotation
/api/cotizaciones/[id]GETOwner or adminGet quotation by ID
/api/cotizaciones/[id]PUTOwner or adminUpdate quotation
/api/cotizaciones/[id]DELETEOwner or adminDelete quotation
/api/cotizaciones/next-nroGETAuthenticatedGenerate next quote number

Master Data

EndpointMethodsAccess Control
/api/clientesGETAny authenticated user
/api/clientesPOSTAdmin only
/api/clientes/[id]PUTAdmin only
/api/clientes/[id]DELETEAdmin only

Analytics

EndpointMethodsAccess ControlPurpose
/api/dashboardGETAuthenticated (filtered by role)Dashboard data with ?tipo= query param
Admin role = DIRECTOR, GERENTE, or ADMINISTRACION (see src/lib/utils.ts:isAdmin() at line 36).

Route Groups

(dashboard) Group

The (dashboard) folder is a route group that:
  • Does not create a URL segment (/dashboard does not exist)
  • Shares a common layout ((dashboard)/layout.tsx)
  • Applies session guard to all nested routes
  • Renders Header and Sidebar for authenticated users
Why use route groups?
  • Organize related routes without affecting URLs
  • Share layouts across multiple route segments
  • Apply authentication guards at a single point

Layout Hierarchy

Root Layout (app/layout.tsx)
├── Applies global styles and fonts
├── Wraps entire application

└── Dashboard Layout ((dashboard)/layout.tsx)
    ├── Checks session with getSession()
    ├── Redirects to /login if not authenticated
    ├── Renders Header component
    ├── Renders Sidebar component (filtered by role)
    └── Renders page content in <main>

Root Layout

// src/app/layout.tsx
import { Work_Sans } from "next/font/google";
import "./globals.css";

const workSans = Work_Sans({ subsets: ["latin"] });

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="es">
      <body className={workSans.className}>{children}</body>
    </html>
  );
}

Dashboard Layout

// src/app/(dashboard)/layout.tsx
import { redirect } from "next/navigation";
import { getSession } from "@/lib/session";
import { Header } from "@/components/Header";
import { Sidebar } from "@/components/Sidebar";

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await getSession();

  if (!session.isLoggedIn) {
    redirect("/login");
  }

  return (
    <div className="flex h-screen">
      <Sidebar role={session.rol} />
      <div className="flex-1 flex flex-col">
        <Header user={session} />
        <main className="flex-1 overflow-auto p-6">
          {children}
        </main>
      </div>
    </div>
  );
}

Dynamic Routes

Quotation Detail [id]

Dynamic route for viewing and editing individual quotations.
// src/app/(dashboard)/cotizaciones/[id]/page.tsx
import { db } from "@/db/db";
import { quotations } from "@/db/schema";
import { eq } from "drizzle-orm";

interface PageProps {
  params: { id: string };
}

export default async function QuotationPage({ params }: PageProps) {
  const quotation = await db
    .select()
    .from(quotations)
    .where(eq(quotations.id, parseInt(params.id)))
    .get();

  if (!quotation) {
    return <div>Cotización no encontrada</div>;
  }

  return <QuotationForm initialData={quotation} mode="edit" />;
}
URL Examples:
  • /cotizaciones/1params.id = "1"
  • /cotizaciones/42params.id = "42"

API Dynamic Routes

API routes also support dynamic segments.
// src/app/api/cotizaciones/[id]/route.ts
import { NextResponse } from "next/server";
import { db } from "@/db/db";
import { quotations } from "@/db/schema";
import { eq } from "drizzle-orm";
import { getSession } from "@/lib/session";

interface RouteContext {
  params: { id: string };
}

export async function GET(
  request: Request,
  { params }: RouteContext
) {
  const session = await getSession();

  if (!session.isLoggedIn) {
    return NextResponse.json(
      { error: "No autorizado" },
      { status: 401 }
    );
  }

  const quotation = await db
    .select()
    .from(quotations)
    .where(eq(quotations.id, parseInt(params.id)))
    .get();

  if (!quotation) {
    return NextResponse.json(
      { error: "No encontrado" },
      { status: 404 }
    );
  }

  // Check ownership
  if (!isAdmin(session.rol) && quotation.user_id !== session.userId) {
    return NextResponse.json(
      { error: "Acceso denegado" },
      { status: 403 }
    );
  }

  return NextResponse.json(quotation);
}

Redirect Logic

Root Page Redirect

// src/app/page.tsx
import { redirect } from "next/navigation";
import { getSession } from "@/lib/session";

export default async function HomePage() {
  const session = await getSession();
  
  if (session.isLoggedIn) {
    redirect("/cotizaciones");
  } else {
    redirect("/login");
  }
}

After Login Redirect

// src/app/login/page.tsx (client component)
"use client";

import { useRouter } from "next/navigation";
import { useState } from "react";

export default function LoginPage() {
  const router = useRouter();
  const [error, setError] = useState("");

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    const res = await fetch("/api/auth/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        email: formData.get("email"),
        password: formData.get("password"),
      }),
    });

    if (res.ok) {
      router.push("/cotizaciones"); // Redirect on success
    } else {
      setError("Credenciales inválidas");
    }
  }

  return <form onSubmit={handleSubmit}>{/* ... */}</form>;
}

No Middleware Implementation

The application does not implement Next.js middleware (middleware.ts). All authentication is handled via:
  • Layout-level session checks (server-side redirect)
  • API route manual guards
This approach is simpler but means authentication logic is duplicated across API routes.
Alternative with Middleware:
// middleware.ts (NOT currently implemented)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { getSession } from "@/lib/session";

export async function middleware(request: NextRequest) {
  const session = await getSession();

  // Protect all routes under /cotizaciones, /dashboard, /maestros
  if (
    request.nextUrl.pathname.startsWith("/cotizaciones") ||
    request.nextUrl.pathname.startsWith("/dashboard") ||
    request.nextUrl.pathname.startsWith("/maestros")
  ) {
    if (!session.isLoggedIn) {
      return NextResponse.redirect(new URL("/login", request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

Best Practices

Use server components for pages that fetch data. Only add "use client" when you need:
  • Event handlers (onClick, onChange, etc.)
  • React hooks (useState, useEffect, etc.)
  • Browser APIs (window, localStorage, etc.)
Keep API routes in app/api/ organized by resource:
app/api/
├── auth/
├── cotizaciones/
├── clientes/
└── usuarios/
Route groups like (dashboard) allow you to:
  • Share layouts without creating URL segments
  • Apply authentication guards at a single point
  • Organize related routes logically
Always check session status at the top of:
  • Protected layouts (redirect if not logged in)
  • API route handlers (return 401 if not authenticated)
  • Server actions (validate before mutations)

Authentication

Learn about iron-session and authorization guards

API Reference

Browse all API endpoints

Next.js App Router

Official Next.js App Router documentation

Database Schema

View complete database schema and relationships

Build docs developers (and LLMs) love