.gemini/skills/backend-patterns/SKILL.md
Backend development patterns for LivestockAI. Use when implementing server functions, database operations, Kysely queries, or working with the three-layer architecture (server → service → repository).
npx skillsauth add captjay98/gemini-livestockai backend-patternsInstall 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.
You are a backend specialist for LivestockAI, expert in TanStack Start, Kysely ORM, and Cloudflare Workers.
Cloudflare Workers does NOT support process.env. Always use async getDb():
// ✅ CORRECT - Works on Cloudflare Workers
export const myFunction = createServerFn({ method: 'POST' })
.inputValidator(schema)
.handler(async ({ data }) => {
const { getDb } = await import('~/lib/db')
const db = await getDb()
// ... use db
})
// ❌ WRONG - Breaks on Cloudflare
import { db } from '~/lib/db'
requireAuth()export const createBatchFn = createServerFn({ method: 'POST' })
.inputValidator(createBatchSchema)
.handler(async ({ data }) => {
const { requireAuth } = await import('../auth/server-middleware')
const session = await requireAuth()
// Validate business rules (service layer)
const error = validateBatchData(data)
if (error) throw new AppError('VALIDATION_ERROR', error)
// Database operation (repository layer)
const { getDb } = await import('~/lib/db')
const db = await getDb()
return insertBatch(db, { ...data, userId: session.user.id })
})
export function calculateFCR(feedKg: number, weightGain: number): number {
if (weightGain <= 0) return 0
return Number((feedKg / weightGain).toFixed(2))
}
export function validateBatchData(data: CreateBatchData): string | null {
if (data.initialQuantity <= 0) return 'Quantity must be positive'
if (data.costPerUnit < 0) return 'Cost cannot be negative'
return null
}
db as parameterexport async function insertBatch(
db: Kysely<Database>,
data: BatchInsert,
): Promise<string> {
const result = await db
.insertInto('batches')
.values(data)
.returning('id')
.executeTakeFirstOrThrow()
return result.id
}
// Always use Zod, never identity functions
const schema = z.object({
farmId: z.string().uuid(),
batchName: z.string().min(1).max(100),
quantity: z.number().int().positive(),
status: z.enum(['active', 'depleted', 'sold']),
date: z.coerce.date(),
notes: z.string().max(500).nullish(),
})
// Prefer explicit columns
const batches = await db
.selectFrom('batches')
.select(['id', 'batchName', 'status', 'quantity'])
.where('farmId', '=', farmId)
.where('deletedAt', 'is', null) // Soft delete
.orderBy('createdAt', 'desc')
.execute()
// Joins
const batchesWithFarm = await db
.selectFrom('batches')
.leftJoin('farms', 'farms.id', 'batches.farmId')
.select(['batches.id', 'batches.batchName', 'farms.name as farmName'])
.execute()
// Transactions
await db.transaction().execute(async (trx) => {
await trx.insertInto('mortality_records').values(data).execute()
await trx
.updateTable('batches')
.set({ quantity: sql`quantity - ${data.count}` })
.where('id', '=', data.batchId)
.execute()
})
import { AppError } from '~/lib/errors'
// Throw structured errors
throw new AppError('NOT_FOUND', 'Batch not found')
throw new AppError('UNAUTHORIZED', 'Access denied')
throw new AppError('VALIDATION_ERROR', 'Invalid input')
data-ai
Input validation patterns with Zod in LivestockAI server functions
testing
Unit testing patterns with Vitest in LivestockAI
tools
Server → Service → Repository pattern for feature organization
data-ai
Server-side rendering and server functions with TanStack Start in LivestockAI