.claude/skills/glapi-backend-guidelines/SKILL.md
GLAPI backend development guide for Next.js + TRPC + Drizzle ORM + PostgreSQL + Clerk. TRPC for internal type-safety, REST API exposure via OpenAPI. Covers layered architecture (routers → services → Drizzle), dual TRPC/REST endpoints, Clerk authentication, and testing strategies.
npx skillsauth add adteco/glapi glapi-backend-guidelinesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Establish consistency and best practices for GLAPI's backend development using:
Automatically activates when working on:
Your Unique Architecture:
Client Request (Frontend)
↓
┌─────────────────┐
│ TRPC (Internal) │ ← Next.js frontend
│ Type-safe │
└─────────────────┘
↓
Service Layer ← Shared business logic
↓
Drizzle ORM
↓
PostgreSQL
External Request (API)
↓
┌──────────────────┐
│ REST API Routes │ ← External clients
│ (Next.js) │
└──────────────────┘
↓
TRPC Procedures (reuse)
↓
Service Layer
↓
Drizzle ORM
↓
PostgreSQL
HTTP/TRPC Request
↓
TRPC Router OR Next.js API Route (thin controller)
↓
Service Layer (business logic)
↓
Drizzle ORM (data access)
↓
Database (PostgreSQL)
Key Principle: Each layer has ONE responsibility.
apps/
api/
src/
app/
api/ # REST API routes (external)
users/
[id]/
route.ts # GET/PATCH/DELETE /api/users/:id
route.ts # GET/POST /api/users
web/
src/
app/
api/ # REST API routes (can be here too)
server/
routers/
_app.ts # Main router composition
users.router.ts # TRPC routers
posts.router.ts
trpc.ts # TRPC setup + procedures
context.ts # TRPC context creation
packages/
database/
src/
schema/ # Drizzle schemas
users.ts
posts.ts
index.ts # Export all schemas
index.ts # Export db + helpers
drizzle.config.ts
services/
src/
user.service.ts # Business logic services
post.service.ts
index.ts # Export all services
Naming Conventions:
camelCase.router.ts - users.router.tsroute.ts in directory structurecamelCase.service.ts - user.service.tscamelCase.ts - users.tspublicProcedure or protectedProcedureapps/web/src/app/api/ or apps/api/src/app/api/auth()packages/services/src/// ❌ NEVER: Business logic in routers
export const usersRouter = createTRPCRouter({
create: publicProcedure
.input(createUserSchema)
.mutation(async ({ input }) => {
// 200 lines of business logic here ❌
const user = await db.insert(users).values(input);
await sendWelcomeEmail(user);
await createDefaultSettings(user);
// ... more logic
}),
});
// ✅ ALWAYS: Delegate to service
export const usersRouter = createTRPCRouter({
create: publicProcedure
.input(createUserSchema)
.mutation(async ({ input, ctx }) => {
return userService.createUser(input);
}),
});
// packages/services/src/user.service.ts
export const userService = {
async createUser(data: CreateUserInput) {
// All business logic here ✅
const [user] = await db.insert(users).values(data).returning();
await this.sendWelcomeEmail(user);
await this.createDefaultSettings(user);
return user;
},
};
// ❌ NEVER: Manual auth checks
export const usersRouter = createTRPCRouter({
getProfile: publicProcedure.query(async ({ ctx }) => {
if (!ctx.userId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
// ...
}),
});
// ✅ ALWAYS: Use protectedProcedure
export const usersRouter = createTRPCRouter({
getProfile: protectedProcedure.query(async ({ ctx }) => {
// ctx.userId is guaranteed to exist (from Clerk)
return userService.getProfile(ctx.userId);
}),
});
import { z } from "zod";
// Define schemas
const createUserSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email"),
age: z.number().min(18, "Must be 18+").optional(),
});
// Use in procedure
export const usersRouter = createTRPCRouter({
create: publicProcedure
.input(createUserSchema)
.mutation(async ({ input }) => {
// input is typed and validated! ✅
return userService.create(input);
}),
});
// ❌ NEVER: Wrapper objects in TRPC
return {
success: true,
data: user,
message: "User created",
};
// ✅ ALWAYS: Return data directly in TRPC
return user;
// Exception: REST API routes may use standard structure
// This is fine in Next.js API routes:
return NextResponse.json({ success: true, data: user });
import { db } from '@glapi/database';
import { users } from '@glapi/database/schema';
import { eq, and } from 'drizzle-orm';
// ❌ NEVER: Raw SQL
const userList = await db.execute(sql`SELECT * FROM users WHERE id = ${id}`);
// ✅ ALWAYS: Drizzle query builder
const user = await db.query.users.findFirst({
where: eq(users.id, id),
});
// OR using select builder
const [user] = await db
.select()
.from(users)
.where(eq(users.id, id))
.limit(1);
// apps/web/src/app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs';
import { appRouter } from '@/server/routers/_app';
import { createContext } from '@/server/trpc';
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { userId } = auth();
// Create TRPC context
const ctx = await createContext({ userId });
// Call TRPC procedure ✅
const caller = appRouter.createCaller(ctx);
const user = await caller.users.getById({ id: params.id });
return NextResponse.json(user);
} catch (error) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
}
import { auth, currentUser } from '@clerk/nextjs';
// In TRPC context
export const createContext = async ({ userId }: { userId?: string | null }) => {
return {
userId, // From Clerk
};
};
// In protected procedure
export const protectedProcedure = publicProcedure.use(async ({ ctx, next }) => {
if (!ctx.userId) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
userId: ctx.userId,
},
});
});
// In Next.js API route
export async function GET(request: NextRequest) {
const { userId } = auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Protected logic here
}
See resources/complete-examples.md for full code examples including:
import { z } from 'zod';
import { createTRPCRouter, publicProcedure, protectedProcedure } from '../trpc';
import { myService } from '@glapi/services';
export const myRouter = createTRPCRouter({
// Public query
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return myService.getById(input.id);
}),
// Protected mutation
create: protectedProcedure
.input(z.object({
name: z.string(),
description: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
return myService.create(input, ctx.userId);
}),
// List with pagination
list: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(50),
offset: z.number().min(0).default(0),
}))
.query(async ({ input }) => {
return myService.list(input);
}),
});
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs';
import { appRouter } from '@/server/routers/_app';
import { createContext } from '@/server/trpc';
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const ctx = await createContext({});
const caller = appRouter.createCaller(ctx);
const item = await caller.myRouter.getById({ id: params.id });
return NextResponse.json(item);
} catch (error) {
return NextResponse.json(
{ error: 'Not found' },
{ status: 404 }
);
}
}
export async function POST(request: NextRequest) {
try {
const { userId } = auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const ctx = await createContext({ userId });
const caller = appRouter.createCaller(ctx);
const created = await caller.myRouter.create(body);
return NextResponse.json(created, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create' },
{ status: 500 }
);
}
}
import { db } from '@glapi/database';
import { myTable } from '@glapi/database/schema';
import { eq } from 'drizzle-orm';
import { TRPCError } from '@trpc/server';
export const myService = {
async getById(id: string) {
const item = await db.query.myTable.findFirst({
where: eq(myTable.id, id),
});
if (!item) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Item not found',
});
}
return item;
},
async create(data: CreateInput, userId: string) {
const [item] = await db.insert(myTable)
.values({
...data,
userId,
})
.returning();
return item;
},
async list({ limit, offset }: ListInput) {
const items = await db.query.myTable.findMany({
limit,
offset,
orderBy: (myTable, { desc }) => [desc(myTable.createdAt)],
});
return items;
},
};
Remember: TRPC for internal type-safety, REST for external access, services for shared logic, and Drizzle for database operations. Keep it simple, type-safe, and DRY!
tools
Create and manage Claude Code skills following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns, file paths, content patterns), enforcement levels (block, suggest, warn), hook mechanisms (UserPromptSubmit, PreToolUse), session tracking, and the 500-line rule.
development
# Pre-Deployment Checklist Skill ## Purpose Run comprehensive quality checks before committing code and creating pull requests. This skill ensures code quality, documentation, API specs, and tests are all in order before deployment. ## When to Use This Skill - Before committing significant changes - Before creating a pull request - When user invokes `/ship`, `/pre-deploy`, or `/checklist` - When preparing code for production deployment ## Checklist Items ### 1. TypeScript Compilation **Com
development
Row Level Security (RLS) implementation guide for GLAPI multi-tenant database isolation. Covers PostgreSQL RLS policies, session variables, contextual database connections, tRPC middleware, and common troubleshooting. Use when working with RLS, multi-tenancy, organization isolation, database security, or debugging RLS policy violations.
development
Frontend development guidelines for Adteco's React 18/19 + ShadCN/Radix UI + Tailwind stack. Patterns for building accessible, performant components with type safety, TanStack Query data fetching, and modern styling.