Files
fullstack-starter/apps/server/AGENTS.md

6.6 KiB

AGENTS.md - Server App Guidelines

TanStack Start fullstack web app with ORPC (contract-first RPC).

Tech Stack

⚠️ This project uses Bun — NOT Node.js / npm. All commands use bun. Never use npm, npx, or node.

  • Framework: TanStack Start (React 19 SSR, file-based routing)
  • Runtime: Bun — NOT Node.js
  • Package Manager: Bun — NOT npm / yarn / pnpm
  • Language: TypeScript (strict mode)
  • Styling: Tailwind CSS v4
  • Database: PostgreSQL + Drizzle ORM
  • State: TanStack Query v5
  • RPC: ORPC (contract-first, type-safe)
  • Build: Vite + Nitro

Commands

# Development
bun dev                    # Vite dev server (localhost:3000)
bun db:studio              # Drizzle Studio GUI

# Build
bun build                  # Production build → .output/
bun compile                # Compile to standalone binary (current platform, depends on build)
bun compile:darwin         # Compile for macOS (default: arm64)
bun compile:darwin:arm64   # Compile for macOS arm64
bun compile:darwin:x64     # Compile for macOS x64
bun compile:linux          # Compile for Linux (default: x64)
bun compile:linux:arm64    # Compile for Linux arm64
bun compile:linux:x64      # Compile for Linux x64
bun compile:windows        # Compile for Windows (default: x64)
bun compile:windows:x64    # Compile for Windows x64

# Code Quality
bun fix                    # Biome auto-fix
bun typecheck              # TypeScript check

# Database
bun db:generate            # Generate migrations from schema
bun db:migrate             # Run migrations
bun db:push                # Push schema directly (dev only)

# Testing (not yet configured)
bun test path/to/test.ts   # Run single test
bun test -t "pattern"      # Run tests matching pattern

Directory Structure

src/
├── client/                # Client-side code
│   ├── orpc.client.ts     # ORPC isomorphic client
│   └── query-client.ts    # TanStack Query client
├── components/            # React components
├── routes/                # TanStack Router file routes
│   ├── __root.tsx         # Root layout
│   ├── index.tsx          # Home page
│   └── api/
│       └── rpc.$.ts       # ORPC HTTP endpoint
├── server/                # Server-side code
│   ├── api/               # ORPC layer
│   │   ├── contracts/     # Input/output schemas (Zod)
│   │   ├── middlewares/   # Middleware (db provider, auth)
│   │   ├── routers/       # Handler implementations
│   │   ├── context.ts     # Request context
│   │   ├── server.ts      # ORPC server instance
│   │   └── types.ts       # Type exports
│   └── db/
│       ├── schema/        # Drizzle table definitions
│       └── index.ts       # Database instance
├── env.ts                 # Environment variable validation
├── router.tsx             # Router configuration
├── routeTree.gen.ts       # Auto-generated (DO NOT EDIT)
└── styles.css             # Tailwind entry

ORPC Pattern

1. Define Contract (src/server/api/contracts/feature.contract.ts)

import { oc } from '@orpc/contract'
import { createSelectSchema } from 'drizzle-zod'
import { z } from 'zod'
import { featureTable } from '@/server/db/schema'

const selectSchema = createSelectSchema(featureTable)

export const list = oc.input(z.void()).output(z.array(selectSchema))
export const create = oc.input(insertSchema).output(selectSchema)

2. Implement Router (src/server/api/routers/feature.router.ts)

import { ORPCError } from '@orpc/server'
import { db } from '../middlewares'
import { os } from '../server'

export const list = os.feature.list.use(db).handler(async ({ context }) => {
  return await context.db.query.featureTable.findMany()
})

3. Register in Index Files

// src/server/api/contracts/index.ts
import * as feature from './feature.contract'
export const contract = { feature }

// src/server/api/routers/index.ts
import * as feature from './feature.router'
export const router = os.router({ feature })

4. Use in Components

import { useSuspenseQuery, useMutation } from '@tanstack/react-query'
import { orpc } from '@/client/orpc.client'

const { data } = useSuspenseQuery(orpc.feature.list.queryOptions())
const mutation = useMutation(orpc.feature.create.mutationOptions())

Database Schema (Drizzle)

import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm'

export const myTable = pgTable('my_table', {
  id: uuid().primaryKey().default(sql`uuidv7()`),
  name: text().notNull(),
  createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
  updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()),
})

Code Style

Formatting (Biome)

  • Indent: 2 spaces
  • Quotes: Single '
  • Semicolons: Omit (ASI)
  • Arrow parens: Always (x) => x

Imports

Biome auto-organizes:

  1. External packages
  2. Internal @/* aliases
  3. Type imports (import type { ... })
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { db } from '@/server/db'
import type { ReactNode } from 'react'

TypeScript

  • strict: true
  • noUncheckedIndexedAccess: true - array access returns T | undefined
  • Use @/* path aliases (maps to src/*)

Naming

Type Convention Example
Files (utils) kebab-case auth-utils.ts
Files (components) PascalCase UserProfile.tsx
Components PascalCase arrow const Button = () => {}
Functions camelCase getUserById
Types PascalCase UserProfile

React

  • Use arrow functions for components (Biome enforced)
  • Use useSuspenseQuery for guaranteed data
  • Let React Compiler handle memoization (no manual useMemo/useCallback)

Environment Variables

// src/env.ts - using @t3-oss/env-core
import { createEnv } from '@t3-oss/env-core'
import { z } from 'zod'

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
  },
  clientPrefix: 'VITE_',
  client: {
    VITE_API_URL: z.string().optional(),
  },
})

Critical Rules

DO:

  • Run bun fix before committing
  • Use @/* path aliases
  • Include createdAt/updatedAt on all tables
  • Use ORPCError with proper codes

DON'T:

  • Use npm, npx, node, yarn, pnpm — always use bun / bunx
  • Edit src/routeTree.gen.ts (auto-generated)
  • Use as any, @ts-ignore, @ts-expect-error
  • Commit .env files
  • Use empty catch blocks