Files
alla-allaos-fullstack/.agents/skills/kiranism-shadcn-dashboard/references/route-handlers-drizzle-guide.md
phaichayon 7a390cf0df init
2026-06-11 12:46:57 +07:00

4.2 KiB

Route Handlers + Drizzle Guide

Use this guide when a feature should stop relying on mock data and start using the app's real server boundary.

Default request flow

The preferred flow in this repo is:

component -> service.ts -> /api route handler -> Drizzle -> database

This keeps:

  • React components UI-focused
  • route handlers responsible for auth, org resolution, and HTTP behavior
  • Drizzle access on the server only

Avoid these paths as defaults:

  • component -> mock-api
  • component -> Drizzle directly
  • client component -> external DB client

Build order

1. Schema first

Define tables and relations before writing React code.

Typical pieces:

  • feature table such as orders
  • optional organizationId foreign key
  • audit columns such as createdAt and updatedAt

If the task includes org-aware access, make tenant ownership explicit in the schema.

2. API contract second

In src/features/<feature>/api/types.ts, define:

  • entity shape used by the UI
  • list filters
  • list response
  • detail response
  • mutation payloads

Keep response types stable even if the schema is denormalized underneath.

3. Route handlers third

Implement:

  • src/app/api/<feature>/route.ts for GET list and POST create
  • src/app/api/<feature>/[id]/route.ts for GET, PATCH or PUT, and DELETE

Responsibilities:

  • authenticate the request
  • resolve active organization
  • check membership or role
  • parse request params and body
  • query or mutate via Drizzle
  • return consistent JSON

Example list handler

import { and, count, desc, eq, ilike } from 'drizzle-orm';
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/auth';
import { db } from '@/lib/db';
import { memberships, orders } from '@/db/schema';

export async function GET(request: NextRequest) {
  const session = await auth();
  if (!session?.user?.id) {
    return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
  }

  const { searchParams } = request.nextUrl;
  const page = Number(searchParams.get('page') ?? 1);
  const limit = Number(searchParams.get('limit') ?? 10);
  const search = searchParams.get('search') ?? '';
  const offset = (page - 1) * limit;

  const membership = await db.query.memberships.findFirst({
    where: eq(memberships.userId, session.user.id)
  });

  if (!membership) {
    return NextResponse.json({ message: 'Organization membership required' }, { status: 403 });
  }

  const whereClause = and(
    eq(orders.organizationId, membership.organizationId),
    search ? ilike(orders.customerName, `%${search}%`) : undefined
  );

  const [items, totalRows] = await Promise.all([
    db.select().from(orders).where(whereClause).orderBy(desc(orders.createdAt)).limit(limit).offset(offset),
    db.select({ value: count() }).from(orders).where(whereClause)
  ]);

  return NextResponse.json({
    items,
    totalItems: totalRows[0]?.value ?? 0
  });
}

Service layer pattern

service.ts should call the local route handlers through apiClient.

import { apiClient } from '@/lib/api-client';
import type { Order, OrderFilters, OrdersResponse, OrderMutationPayload } from './types';

export async function getOrders(filters: OrderFilters): Promise<OrdersResponse> {
  const searchParams = new URLSearchParams();
  if (filters.page) searchParams.set('page', String(filters.page));
  if (filters.limit) searchParams.set('limit', String(filters.limit));
  if (filters.search) searchParams.set('search', filters.search);
  if (filters.status) searchParams.set('status', filters.status);
  if (filters.sort) searchParams.set('sort', filters.sort);

  return apiClient<OrdersResponse>(`/orders?${searchParams.toString()}`);
}

export async function createOrder(values: OrderMutationPayload): Promise<Order> {
  return apiClient<Order>('/orders', {
    method: 'POST',
    body: JSON.stringify(values)
  });
}

Migration advice for this repo

If the task is migrating existing code:

  • start with products or users because both already have route-handler shells
  • swap service.ts away from fakeProducts or fakeUsers
  • replace route-handler mock implementations with Drizzle queries
  • keep queries.ts and UI mostly stable where possible

That minimizes UI churn and isolates the migration to the intended seam.