This guide helps you understand the codebase architecture so you can contribute effectively.
Use npm link to test your local changes in another project without publishing to npm.
cd /path/to/archetype-engine
npm install
npm run build
npm linkcd /path/to/my-app
npm link archetype-engineNow my-app uses your local version. After making changes to archetype-engine, run npm run build to see them reflected.
# In your target project
npm unlink archetype-engine
npm install archetype-engine # Install from npm registry instead- Always run
npm run buildafter changes before testing - Use
npm run test:runto verify changes don't break existing functionality - The CLI commands (
npx archetype init,npx archetype generate) will use your linked version
┌─────────────────┐
│ User Input │
│ archetype.config│
└────────┬────────┘
│
┌────────▼────────┐
│ Fluent API │
│ defineEntity() │
│ text(), number()│
│ hasMany(), etc. │
└────────┬────────┘
│
┌────────▼────────┐
│ IR Compilation │
│ EntityDefinition│
│ ↓ │
│ EntityIR │
└────────┬────────┘
│
┌──────────────┴──────────────┐
│ │
┌────────▼────────┐ ┌────────▼────────┐
│ ManifestIR │ │ CLI (cli.ts) │
│ (compiled) │◄─────────│ init / generate │
└────────┬────────┘ └────────┬────────┘
│ │
│ ┌────────▼────────┐
│ │ Template System │
│ │ registry │
│ │ runner │
│ └────────┬────────┘
│ │
└────────────┬───────────────┘
│
┌────────────▼────────────┐
│ Generators │
│ schema, api, hooks, │
│ validation, auth, i18n │
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ Generated Output │
│ generated/db/ │
│ generated/schemas/ │
│ generated/trpc/ │
│ generated/hooks/ │
└─────────────────────────┘
The codebase uses an Intermediate Representation (IR) pattern that separates user-facing APIs from internal processing.
Why? This decouples the fluent builder API from what generators consume, allowing either side to evolve independently.
┌─────────────────────┐ compile() ┌─────────────────────┐
│ EntityDefinition │ ────────────────► │ EntityIR │
│ (user writes this) │ │ (generators use this)│
└─────────────────────┘ └─────────────────────┘
Example flow:
// User writes (EntityDefinition)
const User = defineEntity('User', {
fields: {
email: text().required().email(),
},
})
// Internally compiled to (EntityIR)
{
name: 'User',
fields: {
email: {
type: 'text',
required: true,
unique: false,
validations: [{ type: 'email' }]
}
},
relations: {},
behaviors: { timestamps: true, softDelete: false, audit: false },
auth: false,
protected: { list: false, get: false, create: false, update: false, remove: false }
}All field builders return new objects instead of mutating:
// Each method returns a NEW object
function createTextFieldBuilder(config: FieldConfig): TextFieldBuilder {
return {
_config: config,
required: () => createTextFieldBuilder({ ...config, required: true }),
email: () => createTextFieldBuilder({
...config,
validations: [...config.validations, { type: 'email' }]
}),
// ... other methods
}
}Generators are simple functions that receive compiled IR and return files:
interface Generator {
name: string
description: string
generate(manifest: ManifestIR, ctx: GeneratorContext): GeneratorOutput
}
// GeneratorOutput is either:
type GeneratorOutput = GeneratedFile | GeneratedFile[]Templates bundle generators for a specific tech stack:
const template: Template = {
meta: { id: 'nextjs-drizzle-trpc', ... },
defaultConfig: { outputDir: 'generated', ... },
generators: [
schemaGenerator, // Drizzle ORM tables
validationGenerator, // Zod schemas
apiGenerator, // tRPC routers
hooksGenerator, // React hooks
// ...
],
}// Add to the builder interfaces section
export interface JsonFieldBuilder extends BaseFieldBuilder<JsonFieldBuilder> {
// Add any JSON-specific methods
schema(zodSchema: string): JsonFieldBuilder
}function createJsonFieldBuilder(config: FieldConfig): JsonFieldBuilder {
return {
_config: config,
required: () => createJsonFieldBuilder({ ...config, required: true }),
optional: () => createJsonFieldBuilder({ ...config, required: false }),
unique: () => createJsonFieldBuilder({ ...config, unique: true }),
default: (value: unknown) => createJsonFieldBuilder({ ...config, default: value }),
label: (value: string) => createJsonFieldBuilder({ ...config, label: value }),
schema: (zodSchema: string) => createJsonFieldBuilder({
...config,
validations: [...config.validations, { type: 'jsonSchema', value: zodSchema }]
}),
}
}/**
* Create a JSON field builder
*
* @returns JsonFieldBuilder with chainable methods
*
* @example
* ```typescript
* json().schema('z.object({ foo: z.string() })')
* ```
*/
export function json(): JsonFieldBuilder {
return createJsonFieldBuilder({
type: 'json', // Add to FieldConfig type union
required: false,
unique: false,
validations: []
})
}export interface FieldConfig {
type: 'text' | 'number' | 'boolean' | 'date' | 'json' // Add 'json'
// ...
}In src/templates/nextjs-drizzle-trpc/generators/schema.ts:
function mapFieldType(config: FieldConfig, isSqlite: boolean): string {
switch (config.type) {
case 'json': return 'text' // Store as JSON string
// ...
}
}export { text, number, boolean, date, json } from './fields'Exact-length validation for fields like country codes, card numbers:
countryCode: text().required().length(2) // Exactly 2 charactersInternally adds both minLength and maxLength validations.
Nullable foreign keys for optional relationships:
customer: hasOne('Customer').optional() // Guest checkout support
parent: hasOne('Category').optional() // Top-level categoriesThe schema generator respects this by NOT adding .notNull() to the FK column.
Create tests in tests/fields.test.ts:
describe('json field', () => {
it('creates json field with defaults', () => {
const field = json()
expect(field._config.type).toBe('json')
expect(field._config.required).toBe(false)
})
it('supports schema validation', () => {
const field = json().schema('z.object({})')
expect(field._config.validations).toContainEqual({
type: 'jsonSchema',
value: 'z.object({})'
})
})
})Create src/templates/nextjs-drizzle-trpc/generators/myfeature.ts:
/**
* MyFeature generator
*
* Generates [description of what this generates].
*
* @module generators/myfeature
*/
import type { Generator, GeneratedFile } from '../../../template/types'
import type { GeneratorContext } from '../../../template/context'
import type { ManifestIR } from '../../../manifest'
/**
* Generate content for a single entity
*/
function generateEntityContent(entity: EntityIR, manifest: ManifestIR): string {
// Your generation logic here
return `// Generated content for ${entity.name}`
}
/**
* MyFeature generator - generates [description]
*
* Generated files:
* - myfeature/{entity}.ts - [description]
*/
export const myFeatureGenerator: Generator = {
name: 'my-feature',
description: 'Generate [description]',
generate(manifest: ManifestIR, ctx: GeneratorContext): GeneratedFile[] {
return manifest.entities.map(entity => ({
path: `myfeature/${entity.name.toLowerCase()}.ts`,
content: generateEntityContent(entity, manifest),
}))
},
}In src/templates/nextjs-drizzle-trpc/index.ts:
import { myFeatureGenerator } from './generators/myfeature'
export const template: Template = {
// ...
generators: [
schemaGenerator,
validationGenerator,
myFeatureGenerator, // Add here
// ...
],
}The ctx parameter provides helpful utilities:
generate(manifest: ManifestIR, ctx: GeneratorContext): GeneratedFile[] {
// Naming utilities
const tableName = ctx.naming.getTableName('BlogPost') // 'blog_posts'
const columnName = ctx.naming.getColumnName('firstName') // 'first_name'
// Database type checks
if (ctx.database.isSqlite) { /* SQLite-specific code */ }
if (ctx.database.isPostgres) { /* PostgreSQL-specific code */ }
// ...
}Templates live in src/templates/. To create a new one:
src/templates/my-new-template/
├── index.ts # Template definition
└── generators/
├── schema.ts # Database schema generator
├── validation.ts # Validation generator
└── ...
import type { Template } from '../../template/types'
import { schemaGenerator } from './generators/schema'
export const template: Template = {
meta: {
id: 'my-new-template',
name: 'My New Template',
description: 'Description for CLI help',
framework: 'nextjs', // or 'sveltekit', 'remix', etc.
stack: {
database: 'drizzle',
validation: 'zod',
api: 'trpc',
ui: 'react',
},
},
defaultConfig: {
outputDir: 'generated',
importAliases: {
'@/generated': 'generated',
},
},
generators: [
schemaGenerator,
// Add your generators here
],
}
export default templateIn src/template/registry.ts:
const templates: Record<string, () => Promise<Template>> = {
'nextjs-drizzle-trpc': async () =>
(await import('../templates/nextjs-drizzle-trpc')).default,
'my-new-template': async () =>
(await import('../templates/my-new-template')).default,
}src/
├── index.ts # Package entry point, exports public API
├── cli.ts # CLI entry point (init, generate, view commands)
├── fields.ts # Field builders (text, number, boolean, date)
├── relations.ts # Relation builders (hasOne, hasMany, belongsToMany)
├── entity.ts # Entity definition and IR compilation
├── manifest.ts # Manifest definition and IR compilation
├── source.ts # External source configuration
├── core/
│ └── utils.ts # Shared utilities (naming, pluralization)
├── template/
│ ├── index.ts # Template system exports
│ ├── types.ts # Template & Generator interfaces
│ ├── context.ts # GeneratorContext with utilities
│ ├── runner.ts # Template execution
│ └── registry.ts # Template lazy-loading registry
├── templates/
│ └── nextjs-drizzle-trpc/
│ ├── index.ts # Template definition
│ └── generators/
│ ├── schema.ts # Drizzle ORM schema
│ ├── auth.ts # Auth.js integration
│ ├── validation.ts # Zod schemas
│ ├── api.ts # tRPC routers
│ ├── hooks.ts # React hooks
│ ├── service.ts # External API services
│ └── i18n.ts # Translation files
├── init/ # Init command implementation
├── validation/ # Structured validation with error codes
├── json/ # JSON input parsing for AI agents
├── ai/ # AI toolkit (ManifestBuilder, adapters)
└── generators/ # Standalone generators (ERD)
Run tests with:
npm run test:run # Single run
npm run test # Watch modeTests are in tests/ and use Vitest. Each module has its own test file:
entity.test.ts- Entity definition testsfields.test.ts- Field builder testsrelations.test.ts- Relation testsmanifest.test.ts- Manifest compilationvalidation.test.ts- Validation rulesjson-input.test.ts- JSON parsingai.test.ts- AI module tests
- Immutable patterns - Return new objects, don't mutate
- Functional style - Pure functions where possible
- Explicit types - Use TypeScript interfaces for public APIs
- JSDoc comments - Document public functions with examples
- Consistent naming:
- PascalCase for types/interfaces:
EntityIR,FieldConfig - camelCase for functions/variables:
defineEntity,createTextFieldBuilder - snake_case for database columns:
created_at,organization_id
- PascalCase for types/interfaces:
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes
- Add tests for new functionality
- Run tests:
npm run test:run - Build:
npm run build - Commit with a clear message
- Open a PR against
main
Open an issue on GitHub or check CLAUDE.md for additional project context.