This document captures repeatable patterns and workflows for scaffolding new full-stack projects with AI agents. These patterns are derived from the ShieldTap codebase and are intended as a template for future projects.
- Project Architecture Patterns
- Critical Code Style Standards
- Recommended Code Style Standards
- API Development Workflow
- Database Development Workflow
- Git Workflow Standards
- Environment & Configuration
- Service Class Pattern
CRITICAL: Use monorepo-lite structure with separate backend and frontend directories.
project-root/
├── .gitignore
├── README.md
├── LICENSE
├── AGENTS.md # AI agent guidelines
├── CODE_GUIDELINES.md # This file
├── api/ # Backend (Cloudflare Workers/Hono)
│ ├── package.json
│ ├── tsconfig.json
│ ├── wrangler.toml
│ ├── src/
│ │ ├── index.ts # Main app with routes
│ │ ├── types.ts # Centralized types
│ │ ├── openapi.ts # OpenAPI specification
│ │ ├── middleware/ # Auth, CORS, etc.
│ │ ├── services/ # Business logic (classes)
│ │ └── db/ # Database schema
├── mobile/ # Mobile app (React Native)
├── web/ # Web app (future)
└── marketing/ # Marketing site (future)
CRITICAL: Maintain strict separation between layers.
- Routes (
index.ts): Thin handlers that delegate to services - Services (
services/): Business logic implemented as classes - Middleware (
middleware/): Cross-cutting concerns (auth, CORS) - Database (
db/): Schema and migrations only
CRITICAL: All types centralized in types.ts, not scattered across files.
CRITICAL: Use function declarations, NOT arrow functions for top-level exports.
// Good
export function createAuthMiddleware() {
return createMiddleware(/* ... */);
}
// Bad - DO NOT USE
export const createAuthMiddleware = () => { /* ... */ };CRITICAL: Always use .ts extensions in import statements.
import { Hono } from 'hono';
import { UserService } from './services/UserService.ts';
import type { User } from './types.ts';CRITICAL: Use Bun exclusively, never npm.
bun install
bun add package-name
bun run script-nameCRITICAL: Use descriptive variable names that clearly indicate what the value represents.
- Include units when dealing with time-related values
- Use specific names that describe the data type and purpose
- Avoid generic names that don't provide context
// Good - descriptive names with units
const currentUnixTimestamp = Math.floor(Date.now() / 1000);
const currentDateTime = new Date();
const userResponseData = await response.json();
const tapCountResult = await db.prepare('SELECT COUNT(*) as count FROM taps').first();
// Good - descriptive names without units when context is clear
const userId = session.user.id;
const userName = user.name;
const tapType = 'resist';
// Bad - non-descriptive or generic
const now = Math.floor(Date.now() / 1000);
const now = new Date();
const data = await response.json();
const result = await db.prepare('SELECT COUNT(*) as count FROM taps').first();RECOMMENDED: Use import type for type-only imports.
import type { User, Bindings } from './types.ts';RECOMMENDED: Single-line JSDoc comments above methods and endpoints.
// Creates a new tap record for a user
async create(userId: string, input: CreateTapInput): Promise<Tap> {
// implementation
}
// Records a new tap (resist or yield) for the authenticated user
app.post('/api/taps', async (c) => { /* ... */ });RECOMMENDED: Use ESNext target, strict mode, and allow .ts extensions.
{
"compilerOptions": {
"target": "ESNext",
"strict": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true
}
}CRITICAL: MUST provide OpenAPI specification for all endpoints.
- Create/Update
openapi.tswhen adding/modifying routes - Define schemas in
components.schemas - Include examples for request/response
- Document authentication requirements
CRITICAL: MUST provide Swagger UI for interactive testing (dev-only).
// Serve OpenAPI spec as JSON
app.get('/openapi.json', (c) => c.json(openAPISpec));
// Serve Swagger UI (dev-only)
app.get('/docs', async (c) => {
if (c.env.DEV_MODE !== 'true') {
return c.json({ error: 'Documentation is only available in development mode' }, 404);
}
const { swaggerUI } = await import('@hono/swagger-ui');
return swaggerUI({ url: '/openapi.json' })(c);
});CRITICAL: Use dynamic import to avoid bundling Swagger UI in production.
CRITICAL: Thin handlers that delegate to services.
app.get('/api/me', async (c) => {
const authUser = c.get('user');
const userService = new UserService(c.env.DB);
const user = await userService.findOrCreate(authUser);
return c.json({ user });
});CRITICAL: Schema as SQL file in db/schema.sql.
-- Users table
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
created_at INTEGER DEFAULT (unixepoch())
);
-- Index for efficient queries
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);CRITICAL: MUST use prepared statements with .bind() for all queries.
const result = await this.db
.prepare('SELECT * FROM users WHERE id = ?')
.bind(userId)
.first<User>();CRITICAL: Type all database queries.
.first<User>() // Single row, typed
.all<Tap[]>() // All rows, typed
.first<{ count: number }>() // Inline type for aggregatesRECOMMENDED: Use IF NOT EXISTS for schema changes.
CREATE TABLE IF NOT EXISTS taps (/* ... */);
CREATE INDEX IF NOT EXISTS idx_taps_user ON taps(user_id);CRITICAL: MUST use conventional commits.
Pattern: <type>: <short description>
Types:
feat:- New featurerefactor:- Code refactoringchore:- Maintenance (formatting, docs)Initial commit- Repository initialization
Examples:
feat: add user authentication
refactor: extract service layer
chore: update dependencies
Initial commitRECOMMENDED: Feature branches from main.
git checkout main
git checkout -b feature/add-feature-name
# work on feature
git push --set-upstream origin feature/add-feature-nameCRITICAL: MUST push all changes to remote repository.
git add .
git commit -m "type: description"
git push
# Verify: git status shows "up to date with origin"RECOMMENDED: Standard ignore patterns.
node_modules/
dist/
.wrangler/
.env
.env.*
!.env.example
*.log
coverage/
.DS_Store
CRITICAL: MUST use DEV_MODE flag for dev/prod separation.
In wrangler.toml:
[vars]
DEV_MODE = "true" # Local development
# DEV_MODE = "false" # Production (change before deploy)In code:
// Check for dev mode
if (c.env.DEV_MODE !== 'true') {
// Production-only behavior
}
// Bypass auth in dev mode
if (c.env.DEV_MODE === 'true') {
const devUser = { id: 'dev-user-id', email: 'dev@example.com' };
c.set('user', devUser);
return;
}CRITICAL: Use string comparison === 'true', not boolean.
RECOMMENDED: Access via bindings interface.
interface Bindings {
DB: D1Database;
TEAM_DOMAIN: string;
POLICY_AUD: string;
DEV_MODE: string;
}
// In route handler
const db = c.env.DB;
const teamDomain = c.env.TEAM_DOMAIN;RECOMMENDED: Define all Cloudflare config in wrangler.toml.
name = "app-name"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
TEAM_DOMAIN = "https://yourteam.cloudflareaccess.com"
POLICY_AUD = "your-policy-aud-here"
DEV_MODE = "true"
[[d1_databases]]
binding = "DB"
database_name = "app-db"
database_id = "local-dev-db"CRITICAL: Services MUST be classes with constructor dependency injection.
export class UserService {
private db: D1Database;
constructor(db: D1Database) {
this.db = db;
}
async findById(id: string): Promise<User | null> {
const result = await this.db
.prepare('SELECT * FROM users WHERE id = ?')
.bind(id)
.first<User>();
return result ?? null;
}
}CRITICAL: Declare properties explicitly in the class and assign them in the constructor body.
CRITICAL: All service methods MUST be async returning Promise<T>.
async create(input: CreateInput): Promise<T> { /* ... */ }
async list(query: Query): Promise<T[]> { /* ... */ }
async getStats(id: string): Promise<Stats> { /* ... */ }RECOMMENDED: Use findOrCreate pattern where appropriate.
async findOrCreate(id: string): Promise<User> {
const existing = await this.findById(id);
if (existing) {
return existing;
}
return this.create(id);
}