Skip to content

Coding Rules

Naming Conventions

Target Convention Example
Variables and functions camelCase getCandidateById
Types and interfaces PascalCase ArrangeSettings
File names kebab-case survey-import.ts / use-project-arrange.ts
Constants UPPER_SNAKE_CASE VALID_STATUS_TRANSITIONS
DB tables and columns snake_case (plural tables) project_candidates.external_user_id
Public API fields snake_case interview_duration_minutes
Internal code from services down camelCase interviewDurationMinutes
React components PascalCase (files kebab-case) SegmentSidebar / segment-sidebar.tsx
Orval-generated hooks Derived from the path (do not rename) useGetApiV1ProjectsProjectId

Where snake_case โ†” camelCase conversion happens

Public API fields are snake_case, internals are camelCase. The conversion is performed explicitly inside the route handler (data.interview_duration_minutes โ†’ interviewDurationMinutes). Never let snake_case leak into the service layer.

Formatter and Linter

# backend
bun run format        # prettier --write 'src/**/*.ts'
bun run lint          # eslint src/
bun run lint:fix
bun run check-types   # tsc --noEmit

# frontend
bun run format
bun run lint
bun run check-types

Configuration files:

File Contents
.prettierrc.mjs semi: true / singleQuote: true / printWidth: 100 / trailingComma: 'es5' / tabWidth: 2
eslint.config.js (backend) @typescript-eslint recommended plus custom rules
eslint.config.mjs (frontend) eslint-config-next plus prettier

Notable backend ESLint rules:

Rule Setting
@typescript-eslint/no-unused-vars error, with an _ prefix exemption
@typescript-eslint/no-explicit-any warn
no-console warn (only console.warn / console.error allowed; use logger normally)
@typescript-eslint/explicit-function-return-type off

Both repositories run prettier --write and eslint --fix over src/** on commit via husky + lint-staged.

Backend Conventions

Imports carry the ESM .js extension

import { response } from '../lib/index.js';        // โœ…
import { response } from '../lib/index';           // โŒ

This applies even when the target is a TypeScript source file.

Always return through the response helper

Use response.success / created / paginated / error from src/lib/response.ts. The body is always wrapped in the envelope.

return response.success(c, project);
return response.created(c, project);
return response.paginated(c, items, pagination);

Never call c.json() directly.

Just throw errors

middlewares/error-handler.ts converts them into status / code / message. Do not build error responses with try-catch inside handlers.

if (!project) throw new NotFoundError('Project');             // 404
throw new InvalidStatusTransitionError(oldStatus, newStatus); // 400
Location Contents
src/lib/errors.ts Generic (HTTP / DB / external API / Databricks)
src/error.ts Domain-specific (Google API, scheduling tokens, slot conflicts, status transitions)

Errors with isOperational === false do not expose details to the client (stack traces only in development).

Do not skip layers

Preserve the one-way dependency routes โ†’ services โ†’ repositories โ†’ Drizzle.

  • Never call Drizzle from a route
  • Never pass Hono's Context into a service
  • Never put business decisions in a repository

Read environment variables through getEnvConfig()

Do not read process.env directly. In particular, use the dedicated helpers for PROTOTYPE_MODE.

if (isEmailEnabled()) { ... }             // โœ…
if (isCalendarEnabled()) { ... }          // โœ…
if (process.env.PROTOTYPE_MODE) { ... }   // โŒ

Batches (src/batch/*.ts) are the exception: they read DATABASE_URL directly so they can run where the app's required variables are absent.

Write the route definition next to its handler

const getProjectRoute = createRoute({
  method: 'get',
  path: '/{projectId}',
  tags: ['Projects'],
  summary: 'Get project detail',
  security: [{ bearerAuth: [] }],
  request: { params: ProjectIdParamSchema },
  responses: { 200: { ... }, 404: commonResponses[404] },
});

projectsRoutes.openapi(getProjectRoute, async (c) => { ... });

Reuse commonResponses for error responses.

Frontend Conventions

src/api/** is generated โ€” never edit it

After changing the API, start the backend and run bun run generate-api.

src/lib/auth.ts is the only gateway for auth tokens

Do not touch localStorage directly. Use setAuthToken / getAuthToken / removeAuthToken.

Unwrap API responses with apiData()

const { data } = useGetApiV1Projects();
const projects = apiData<GetApiV1Projects200>(data);

Do not hand-traverse response.data.data.

Where state lives

Kind Location
Server state React Query (Orval-generated hooks)
Local UI state useState in the component
Shared providers src/components/providers.tsx
Arrange flow state Concentrated in use-project-arrange.ts; keep screens thin

Compose class names with cn()

import { cn } from '@/lib/utils';
<div className={cn('px-4', isActive && 'bg-blue-500')} />

About the Duplicated Type

ArrangeSettings (the contents of the projects.arrange_settings JSON column) is defined in two places because OpenAPI cannot express the internal structure of a JSON column.

File Purpose
backend src/db/schema.ts DB and service side
frontend src/types/arrange-settings.ts Screen side

Change one and you must change the other.

PR and Review Standards

  • One PR, one purpose
  • Do not mix in unrequested features, refactors, or abstractions
  • A PR that changes the API must include the resulting src/api/** diff from bun run generate-api
  • A PR that changes the DB schema must also update the Database docs
  • Self-review before requesting review
  • Never use force pushes

Anti-patterns

  • Overusing any (only a warning, but avoid it on principle)
  • Inline console.log (use logger; only console.warn / console.error are allowed)
  • Calling Drizzle from a route handler
  • Calling c.json() directly (use the response helper)
  • Building error responses with try-catch inside handlers (just throw)
  • Reading process.env directly
  • Hand-editing generated files (src/api/**)
  • Adding an environment variable to src/config/env.ts but forgetting .env.example
  • Inline magic numbers (extract constants)
  • Deep nesting (use early returns)