Code Guidelines
This repo is a demo. Patterns here are suggestions; swap for what fits your team.
Dependency Injection
Prefer constructor/function injection for side effects:
// Good: injectable
function createUserService(db: Database, logger: Logger) {
return { ... }
}
// Bad: global import
import { db } from '../db'
- Wire at edges (app startup, router factories)
- Avoid global singleton imports from deep modules
- Tests supply fakes without patching globals
Error Handling (neverthrow)
Use Result<T, E> for explicit success/error flow.
// Services return Result
function findUser(id: string): ResultAsync<User, NotFoundError | DbError>
// Validate input early
const validated = validateInput(schema, input);
if (validated.isErr()) return err(validated.error);
// Wrap throwy code once
return await fromAsyncThrowable(
async () => dbCall(),
(e) => typedError(e),
)();
Error mapping:
- auth/ownership → 401/403
- missing resources → 404
- validation → 400
- unexpected → 500 (log with context)
Helpers: packages/backend/core/src/validation.ts (validateInput, typedError)
Logging (pino)
Use structured logging:
logger.info("user created", { userId: user.id, email: user.email })
logger.error("operation failed", err, { orderId, userId })
Rules:
- Log at boundaries (request → router → service)
- Never log secrets (tokens, passwords, cookies)
- Use levels: error, warn, info, debug
- Optional
REQUEST_LOGGINGflag inapps/backend/api/src/orpc.ts
Location: apps/backend/api/src/log.ts, packages/backend/core/src/log.ts
oRPC (Type-safe RPC)
Type-safe RPC between frontend and backend with React Query helpers.
Server Pattern
// Router factory pattern
orpc.router({
user: userRouter(),
todo: todoRouter(),
});
// Protected procedure with authOnly middleware
orpc.use(authOnly).input(zodSchema).handler(...)
Client Usage
const api = useApi();
// Queries
const todosQuery = useQuery(api.todo.list.queryOptions({ input: { completed: false } }));
// Mutations
const createTodo = useMutation(api.todo.create.mutationOptions({
onSuccess: () => queryClient.invalidateQueries({ queryKey: api.todo.key() })
}));
Key files:
- Server router:
apps/backend/api/src/routers/index.ts - Server setup:
apps/backend/api/src/orpc.ts - Client:
apps/frontend/web/app/providers/orpc-provider.tsx
Auth (Better Auth)
Cookie-based auth with typed user/session in oRPC context.
// Read user in oRPC handlers
const userId = context.user.id;
// Use authOnly middleware for protected procedures
orpc.use(authOnly).handler(...)
CORS requirements:
- Backend:
hono/corswithcredentials: true - Frontend: fetch with
credentials: "include"
Key files:
- Backend:
apps/backend/api/src/auth.ts - Core:
packages/backend/core/src/auth.ts - Frontend:
apps/frontend/web/app/providers/*
Config (env + Zod)
Validate env vars at startup with Zod:
// Backend: parse process.env at module load
const appConfig = configSchema.parse(process.env);
// Frontend: read import.meta.env
const config = getConfig();
Vite env file priority:
.env.[mode].local(git-ignored).env.[mode].env.local(git-ignored).env
Only VITE_* variables exposed to client. Keep .env.*.example in sync.
CI/CD
Local (Husky): On git push:
pnpm lint:checkpnpm format:checkpnpm typecheck
Bypass: HUSKY=0 git push
GitHub Actions: On PR:
- All above +
pnpm test
Workflow: .github/workflows/ci.yml
Tech Choices Summary
| Layer | Choice | Why | |-------|--------|-----| | DB | Kysely | Typed query builder, SQL-first, easy DI | | RPC | oRPC | End-to-end typed, React Query helpers | | Router | TanStack Router | Type-safe, file-based | | Errors | neverthrow | Explicit success/failure flows | | Auth | Better Auth | Free, good coverage, easy swap | | Testing | testcontainers | Real Postgres, shared container for speed |
When Writing Code
- Check existing patterns in similar files
- Use DI for testability
- Handle errors explicitly with Result types
- Add structured logging at boundaries
- Write tests that use DI
- Run
pnpm typecheckandpnpm test