init
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
# 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
|
||||
|
||||
```tsx
|
||||
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`.
|
||||
|
||||
```tsx
|
||||
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.
|
||||
Reference in New Issue
Block a user