Purpose: Development guidance, coding standards, and technical best practices Audience: Current and future contributors to SeatKit Based on: Phase 1 architectural decisions + production Swift app learnings Last Updated: 2025-10-25
seatkit/
โโโ packages/
โ โโโ types/ # Foundation: Zod schemas + TypeScript types
โ โ โโโ depends on: zod
โ โโโ utils/ # Shared utilities
โ โ โโโ depends on: types
โ โโโ engine/ # Business logic & algorithms
โ โ โโโ depends on: types, utils
โ โโโ ui/ # Design system components
โ โ โโโ depends on: types, shadcn/ui
โ โโโ api/ # Backend API server
โ โ โโโ depends on: types, utils, engine
โ โโโ web/ # Frontend application
โ โ โโโ depends on: types, utils, ui, engine (client-safe parts)
โ โโโ config/ # Shared tooling configurations
โโโ tools/ # Development utilities
graph TD
A[config] --> B[types]
B --> C[utils]
B --> D[engine]
B --> E[ui]
C --> F[api]
D --> F
C --> G[web]
E --> G
D --> G
Key Principles:
# Core runtime
node >= 20.0.0 # Primary: Node.js 22.x
pnpm >= 8.0.0 # Package manager
# Development tools (installed via packages)
typescript >= 5.0 # Type checking
turbo >= 1.10 # Monorepo build orchestration
vitest >= 1.0 # Testing framework
eslint >= 8.0 # Code linting
prettier >= 3.0 # Code formatting
# Clone and install
git clone <repository>
cd seatkit
pnpm install
# Development commands
pnpm dev # Start all packages in dev mode
pnpm build # Build all packages
pnpm test # Run all tests
pnpm lint # Lint all code
pnpm type-check # TypeScript checking
# Package-specific commands
pnpm --filter @seatkit/api dev # Run only API in dev mode
pnpm --filter @seatkit/web build # Build only web package
VS Code Settings (.vscode/settings.json):
{
"typescript.preferences.importModuleSpecifier": "relative",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"files.associations": {
"*.css": "tailwindcss"
}
}
Required VS Code Extensions:
Base TypeScript Config (packages/config/tsconfig.base.json):
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"noEmit": true,
// Maximum strictness (Phase 1 decision)
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
// Modern features
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
// Path mapping for monorepo
"baseUrl": ".",
"paths": {
"@seatkit/types": ["../types/src"],
"@seatkit/utils": ["../utils/src"],
"@seatkit/engine": ["../engine/src"],
"@seatkit/ui": ["../ui/src"]
}
}
}
Shared ESLint Config (packages/config/eslint.config.js):
export default [
{
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
rules: {
// Error prevention
'no-unused-vars': 'error',
'no-console': 'warn',
'no-debugger': 'error',
// Code quality
'prefer-const': 'error',
'no-var': 'error',
'object-shorthand': 'error',
// Import organization
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling'],
'newlines-between': 'always',
},
],
},
},
];
Code Formatting (.prettierrc):
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}
PascalCase: Component files, Class files
camelCase: Function files, variable files, regular TypeScript files
kebab-case: Directory names, script files, config files
SCREAMING_CASE: Constants, environment variables
Examples:
โ
Good:
src/
โโโ components/
โ โโโ ReservationCard.tsx
โ โโโ TableLayout.tsx
โ โโโ index.ts
โโโ services/
โ โโโ reservationService.ts
โ โโโ tableService.ts
โ โโโ index.ts
โโโ utils/
โ โโโ dateUtils.ts
โ โโโ validationUtils.ts
โ โโโ constants.ts
โโโ types/
โโโ Reservation.ts
โโโ index.ts
โ Bad:
src/
โโโ Components/ # Should be lowercase
โโโ reservation_service.ts # Should be camelCase
โโโ ReservationUtils.ts # Utils should be camelCase
โโโ table-types.ts # Types should be PascalCase
// โ
Good naming
const reservationList = [...];
const isValidTimeSlot = (time: string) => boolean;
const calculateTableCapacity = (table: Table) => number;
class ReservationService {
private readonly dataRepository: ReservationRepository;
public async createReservation(data: CreateReservationData): Promise<Reservation> {
// ...
}
}
// โ Bad naming
const res_list = [...]; // Abbreviation, snake_case
const checkTime = (t) => {...}; // Unclear function purpose
const calc = (table) => {...}; // Abbreviation
class resService { // Should be PascalCase
private repo; // Should be descriptive
async create(d) { // Unclear parameter
// ...
}
}
// โ
Good type naming
interface Reservation {
id: string;
guestName: string;
partySize: number;
}
type ReservationStatus = 'pending' | 'confirmed' | 'canceled';
type CreateReservationData = Omit<Reservation, 'id'>;
// Zod schemas match their types
export const ReservationSchema = z.object({...});
export type Reservation = z.infer<typeof ReservationSchema>;
// โ Bad type naming
interface IReservation {...} // Avoid Hungarian notation
interface reservationData {...} // Should be PascalCase
type resStatus = string; // Should be descriptive
Purpose: Central type definitions and validation schemas
Structure:
packages/types/
โโโ src/
โ โโโ reservation.ts # Reservation entity + Zod schema
โ โโโ table.ts # Table entity + schema
โ โโโ sales.ts # Sales entities + schemas
โ โโโ user.ts # User/Profile entities + schemas
โ โโโ common.ts # Shared types (dates, IDs, etc.)
โ โโโ index.ts # Re-export all types
โโโ package.json
โโโ README.md
Example Implementation:
// packages/types/src/reservation.ts
import { z } from 'zod';
// Enums first
export const ReservationStatusSchema = z.enum([
'pending',
'confirmed',
'canceled',
'noShow',
'showedUp',
'late',
]);
// Main schema
export const ReservationSchema = z.object({
id: z.string().uuid(),
guestName: z.string().min(1).max(100),
partySize: z.number().int().min(1).max(20),
dateString: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
startTime: z.string().regex(/^\d{2}:\d{2}$/),
status: ReservationStatusSchema,
// ... other fields
});
// Export types
export type Reservation = z.infer<typeof ReservationSchema>;
export type ReservationStatus = z.infer<typeof ReservationStatusSchema>;
// Export creation/update types
export type CreateReservationData = Omit<Reservation, 'id'>;
export type UpdateReservationData = Partial<Omit<Reservation, 'id'>>;
Purpose: Pure business logic, no dependencies on UI or database
Structure:
packages/engine/
โโโ src/
โ โโโ reservation/
โ โ โโโ ReservationService.ts
โ โ โโโ timeSlotValidation.ts
โ โ โโโ conflictResolution.ts
โ โโโ table/
โ โ โโโ TableAssignmentService.ts
โ โ โโโ capacityCalculation.ts
โ โ โโโ layoutAlgorithms.ts
โ โโโ sales/
โ โ โโโ SalesCalculationService.ts
โ โ โโโ analyticsEngine.ts
โ โโโ index.ts
โโโ package.json
โโโ README.md
Design Principles:
Example Service:
// packages/engine/src/reservation/ReservationService.ts
import type { Reservation, CreateReservationData } from '@seatkit/types';
interface ReservationRepository {
save(reservation: Reservation): Promise<void>;
findById(id: string): Promise<Reservation | null>;
findConflicts(
date: string,
startTime: string,
endTime: string,
): Promise<Reservation[]>;
}
export class ReservationService {
constructor(private readonly repository: ReservationRepository) {}
async createReservation(data: CreateReservationData): Promise<Reservation> {
// Validate business rules
await this.validateTimeSlot(data.dateString, data.startTime, data.endTime);
// Create reservation
const reservation: Reservation = {
id: crypto.randomUUID(),
...data,
};
// Save to repository
await this.repository.save(reservation);
return reservation;
}
private async validateTimeSlot(
date: string,
startTime: string,
endTime?: string,
): Promise<void> {
const conflicts = await this.repository.findConflicts(
date,
startTime,
endTime,
);
if (conflicts.length > 0) {
throw new Error('Time slot conflict detected');
}
}
}
packages/engine/
โโโ src/
โ โโโ reservation/
โ โ โโโ ReservationService.ts
โ โ โโโ ReservationService.test.ts # Co-located unit tests
โโโ tests/
โ โโโ integration/ # Cross-service integration tests
โ โโโ fixtures/ # Test data and mocks
Framework: Vitest (chosen for speed and ESM compatibility)
// packages/engine/src/reservation/ReservationService.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ReservationService } from './ReservationService';
describe('ReservationService', () => {
let service: ReservationService;
let mockRepository: MockReservationRepository;
beforeEach(() => {
mockRepository = {
save: vi.fn(),
findById: vi.fn(),
findConflicts: vi.fn().mockResolvedValue([]),
};
service = new ReservationService(mockRepository);
});
describe('createReservation', () => {
it('should create reservation with valid data', async () => {
const reservationData = {
guestName: 'John Doe',
partySize: 2,
dateString: '2025-10-25',
startTime: '19:30',
// ... other required fields
};
const result = await service.createReservation(reservationData);
expect(result.id).toBeDefined();
expect(result.guestName).toBe('John Doe');
expect(mockRepository.save).toHaveBeenCalledWith(result);
});
it('should throw error for conflicting time slots', async () => {
mockRepository.findConflicts.mockResolvedValue([existingReservation]);
const reservationData = {
/* ... */
};
await expect(service.createReservation(reservationData)).rejects.toThrow(
'Time slot conflict detected',
);
});
});
});
// packages/api/tests/integration/reservations.test.ts
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import { app } from '../src/app';
describe('POST /api/reservations', () => {
it('should create reservation and return 201', async () => {
const reservationData = {
guestName: 'John Doe',
partySize: 2,
dateString: '2025-10-25',
startTime: '19:30',
};
const response = await request(app)
.post('/api/reservations')
.send(reservationData)
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
guestName: 'John Doe',
partySize: 2,
});
});
it('should return 400 for invalid data', async () => {
const invalidData = {
guestName: '', // Invalid: empty name
partySize: 0, // Invalid: zero party size
};
const response = await request(app)
.post('/api/reservations')
.send(invalidData)
.expect(400);
expect(response.body.error).toBeDefined();
});
});
// packages/types/src/errors.ts
export abstract class AppError extends Error {
abstract readonly statusCode: number;
abstract readonly code: string;
constructor(
message: string,
public readonly context?: Record<string, unknown>,
) {
super(message);
this.name = this.constructor.name;
}
}
export class ValidationError extends AppError {
readonly statusCode = 400;
readonly code = 'VALIDATION_ERROR';
}
export class ConflictError extends AppError {
readonly statusCode = 409;
readonly code = 'CONFLICT_ERROR';
}
export class NotFoundError extends AppError {
readonly statusCode = 404;
readonly code = 'NOT_FOUND_ERROR';
}
// packages/api/src/middleware/errorHandler.ts
import type { Request, Response, NextFunction } from 'express';
import { AppError } from '@seatkit/types/errors';
import { ZodError } from 'zod';
export function errorHandler(
error: Error,
req: Request,
res: Response,
next: NextFunction,
): void {
if (error instanceof AppError) {
res.status(error.statusCode).json({
error: {
code: error.code,
message: error.message,
...(error.context && { context: error.context }),
},
});
return;
}
if (error instanceof ZodError) {
res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input data',
details: error.errors,
},
});
return;
}
// Unknown error
console.error('Unhandled error:', error);
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred',
},
});
}
// โ
Good: Use proper indexing and selective queries
class ReservationRepository {
async findByDate(date: string): Promise<Reservation[]> {
// Index on (date, start_time) for fast retrieval
return this.db.reservation.findMany({
where: { dateString: date },
orderBy: { startTime: 'asc' },
select: {
// Only select needed fields
id: true,
guestName: true,
startTime: true,
partySize: true,
status: true,
},
});
}
}
// โ Bad: N+1 queries and unnecessary data
class BadReservationRepository {
async findByDate(date: string): Promise<Reservation[]> {
const reservations = await this.db.reservation.findMany({
where: { dateString: date },
});
// N+1 query problem
for (const reservation of reservations) {
reservation.table = await this.db.table.findFirst({
where: { id: reservation.tableId },
});
}
return reservations;
}
}
// โ
Good: Proper memoization and optimizations
import { memo, useMemo } from 'react';
interface ReservationListProps {
reservations: Reservation[];
filter: string;
}
export const ReservationList = memo(({ reservations, filter }: ReservationListProps) => {
// Expensive filtering/sorting memoized
const filteredReservations = useMemo(() => {
return reservations
.filter(r => r.guestName.toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => a.startTime.localeCompare(b.startTime));
}, [reservations, filter]);
return (
<div>
{filteredReservations.map(reservation => (
<ReservationRow key={reservation.id} reservation={reservation} />
))}
</div>
);
});
// Memoize individual rows to prevent unnecessary re-renders
const ReservationRow = memo(({ reservation }: { reservation: Reservation }) => {
return <div>{reservation.guestName} - {reservation.startTime}</div>;
});
// Always validate at boundaries using Zod
app.post('/api/reservations', async (req, res, next) => {
try {
// Parse and validate with Zod
const reservationData = CreateReservationSchema.parse(req.body);
const reservation =
await reservationService.createReservation(reservationData);
res.status(201).json(reservation);
} catch (error) {
next(error);
}
});
// packages/api/src/config.ts
import { z } from 'zod';
const ConfigSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
// Never commit secrets to code
});
export const config = ConfigSchema.parse(process.env);
// JWT token validation middleware
import jwt from 'jsonwebtoken';
export function requireAuth(
req: AuthRequest,
res: Response,
next: NextFunction,
) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const payload = jwt.verify(token, config.JWT_SECRET) as JWTPayload;
req.user = payload;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
}
// Role-based authorization
export function requireRole(role: UserRole) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user || req.user.role !== role) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// turbo.json - Monorepo build pipeline
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "build/**"]
},
"test": {
"dependsOn": ["^build"]
},
"lint": {
"outputs": []
},
"type-check": {
"dependsOn": ["^build"]
}
}
}
# Dockerfile (multi-stage build)
FROM node:22-alpine AS base
RUN npm install -g pnpm turbo
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
COPY packages/*/package.json ./packages/*/
RUN pnpm install --frozen-lockfile
FROM base AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN turbo build
FROM node:22-alpine AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nodejs
COPY --from=builder --chown=nodejs:nodejs /app/packages/api/dist ./
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
USER nodejs
EXPOSE 3000
CMD ["node", "index.js"]
# .env.example
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/seatkit
JWT_SECRET=your-super-secure-jwt-secret-at-least-32-characters-long
CORS_ORIGIN=http://localhost:5173
This technical context document serves as the definitive guide for all SeatKit development. It ensures consistency, maintainability, and quality across all packages and contributors.