Skip to main content

Radish CLI Architecture

Radish CLI generates a complete backend data layer from YAML blueprints. This document explains the generated code architecture and runtime behavior.

Generation Flow

Blueprint YAML → Schema Validation → Code Generation → Architecture Layers
  1. Blueprint Definition - Define entities, fields, and relationships in YAML
  2. Schema Validation - Validate against JSON Schema using @radish/schemas
  3. Code Generation - Generate contracts, models, repos, services, and routes
  4. Architecture Layers - Organized, self-contained packages ready to use

Generated Layers

Radish generates six distinct layers, each with a specific purpose:

1. Contracts Layer

Location: .radish/lib/datalayer/contracts/

  • Zod validation schemas
  • TypeScript type definitions
  • Shared between client and server
  • No runtime dependencies on database

Example:

// Generated contract
export const productSchema = z.object({
name: z.string().min(1),
price: z.number().positive(),
categoryId: z.string()
});

export type Product = z.infer<typeof productSchema>;

2. Models Layer

Location: .radish/lib/datalayer/server/models/

  • Mongoose schema definitions (or Prisma schemas)
  • Database field mappings
  • Index definitions
  • Collection configuration

Automatic Indexing:

  • All ref fields are indexed
  • All fields in filters array are indexed
  • Custom compound indexes from blueprint
  • Automatic deduplication of redundant indexes

3. Repositories Layer

Location: .radish/lib/datalayer/server/repos/

  • CRUD operations (create, read, update, delete)
  • Query building and filtering
  • Pagination (offset and cursor-based)
  • Reference population
  • Soft delete operations (archive/restore)
  • Sorting with collation support

Key Methods:

  • create(data) - Create new record
  • findById(id) - Find by ID
  • list(criteria) - List with filtering, pagination, sorting
  • update(id, data) - Update record
  • archive(id) / restore(id) - Soft delete operations
  • remove(id) - Hard delete (permanent)

4. Services Layer

Location: .radish/lib/datalayer/server/services/

  • Business logic and validation
  • Permission checking
  • Owner scoping
  • Macro value resolution
  • Reference population parsing
  • Cascade operations

This is the primary API layer - application code should use services, not repos directly.

Why Services Over Repos:

  • Enforces authentication and authorization
  • Applies owner scoping automatically
  • Validates input against Zod schemas
  • Processes macros and computed values
  • Consistent error handling

5. Routes Layer

Location: .radish/lib/datalayer/server/routes/

  • HTTP endpoint handlers
  • SvelteKit or Fastify adapters
  • Request/response formatting
  • Error code to HTTP status mapping

Generated Endpoints:

  • GET /api/v1/{plural} - List entities
  • POST /api/v1/{plural} - Create entity
  • GET /api/v1/{plural}/[id] - Get entity
  • PUT /api/v1/{plural}/[id] - Update entity
  • DELETE /api/v1/{plural}/[id] - Delete entity
  • POST /api/v1/{plural}/bulk - Bulk create with validation
  • GET /api/v1/{plural}/metadata - Entity metadata
  • GET /api/v1/schema/metadata - Global schema metadata

6. Metadata Layer

Location: .radish/lib/datalayer/contracts/metadata/

  • Runtime schema introspection
  • Field definitions and types
  • Entity relationships
  • Validation rules
  • UI generation hints

Runtime Request Flow

Client Request

HTTP Route Handler (/api/v1/products)

Service Layer (auth, validation, owner scoping, macros)

Repository Layer (query building, filtering, population)

Model Layer (Mongoose/Prisma)

Database (MongoDB/PostgreSQL)

Example: Creating a Product

  1. Client sends POST /api/v1/products with JSON body
  2. Route Handler extracts body and calls ProductService.create()
  3. Service validates input with CreateProduct Zod schema
  4. Service checks permissions (product:create:own or product:create:all)
  5. Service applies owner scoping (sets ownerId to current user)
  6. Service resolves macros (e.g., {{user.name}} → actual name)
  7. Repository creates the database record
  8. Response returns standardized success envelope with created product

Base Entity Fields

All entities automatically include:

  • _id (or id) - Primary key
  • ownerId - Reference to owning user
  • createdAt - Timestamp of creation
  • updatedAt - Timestamp of last update
  • archivedAt - Soft delete timestamp (null if active)

Reference Population

Radish supports automatic population of related entities to reduce API round-trips:

// Request
GET /api/v1/books?populate=authorId,categoryId

// Response includes expanded objects instead of just IDs
{
"_id": "123",
"title": "Example Book",
"authorId": {
"_id": "456",
"name": "John Doe",
"email": "john@example.com"
},
"categoryId": {
"_id": "789",
"name": "Fiction"
}
}

Population Features:

  • Comma-separated paths: ?populate=authorId,categoryId
  • Nested population: ?populate=authorId.departmentId
  • Respects field permissions (filtered based on user role)
  • Works with list and individual item endpoints
  • Database-agnostic at service layer

Soft Delete Architecture

Radish implements comprehensive soft delete (archiving) to preserve data:

Key Concepts:

  • archivedAt field: null = active, timestamp = archived
  • All queries automatically exclude archived items
  • Archive/restore operations respect permissions
  • Cascade support for related entities

Repository Methods:

  • archive(id) - Soft delete
  • restore(id) - Restore from archive
  • listArchived(criteria) - Query archived items
  • remove(id) - Hard delete (permanent, use with caution)

Service Methods:

  • archive(id) - With permission check
  • restore(id) - With permission check
  • archiveCascade(id) - Archive entity and all dependent entities
  • restoreCascade(id) - Restore entity and all dependencies

Validation & Error Handling

Input Validation

All service methods validate input using Zod schemas:

// Service validates before processing
async create(ctx, input) {
const validated = CreateProduct.parse(input); // Throws if invalid
// ... proceed with creation
}

Automatic Field-Level Errors:

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"name": "Required",
"price": "Must be a positive number"
}
}
}

Standardized Response Envelopes

Success:

{
"data": { /* entity or list */ }
}

Error:

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Human-readable message",
"details": { /* optional */ }
}
}

Error Codes

  • VALIDATION_ERROR (400) - Input validation failed
  • UNAUTHORIZED (401) - Authentication required
  • FORBIDDEN (403) - Permission denied
  • NOT_FOUND (404) - Resource not found
  • CONFLICT (409) - Resource conflict (e.g., duplicate)
  • UNPROCESSABLE_ENTITY (422) - Business rule violation
  • INTERNAL_ERROR (500) - Server error
  • SERVICE_UNAVAILABLE (503) - Temporary unavailability

Sorting & Collation

All repositories support flexible sorting with MongoDB collation:

Default Behavior:

  • Case-insensitive sorting
  • Natural number ordering ("Item 1", "Item 2", "Item 10")
  • Applied at collection level

Custom Sorting:

const items = await repo.list({
sort: {
field: 'title',
direction: 'desc',
collation: { locale: 'en', strength: 2 }
}
});

Limitations:

  • Cannot combine cursor pagination with custom sorting
  • Sort field must exist in schema
  • Invalid fields fall back to _id sort

Permission System

See ../datalayer/access-control.md for complete permission architecture.

Quick Overview:

  • Entity-level permissions (e.g., product:create:own)
  • Action-based (create, read, update, delete, archive, restore)
  • Scope-based (own vs all)
  • Wildcard support (product:*, *:read:all)

Database Indexes

Radish automatically generates optimal indexes:

Automatic Indexing:

  1. All ref fields (foreign keys)
  2. All fields in filters array
  3. Compound indexes from blueprint
  4. Unique indexes from blueprint

Example:

# Blueprint
fields:
authorId: { type: objectId, ref: Author } # Indexed
status: { type: enum, values: [...] } # Indexed (in filters)
filters: [authorId, status, publishedYear]
indexes:
- fields: [isbn]
unique: true
- fields: [authorId, publishedYear]

Generated Indexes:

  • authorId (single, from ref)
  • status (single, from filters)
  • publishedYear (single, from filters)
  • isbn (unique)
  • [authorId, publishedYear] (compound)

File Naming Conventions

  • Models: entity.model.ts exports EntityModel
  • Repositories: entity.repository.ts exports EntityRepository
  • Services: entity.service.ts exports EntityService
  • Routes: /api/v1/{plural}/+server.ts (SvelteKit) or {plural}.route.ts (Fastify)
  • Metadata: /api/v1/{plural}/metadata/* for entity introspection

Metadata Endpoints

Runtime schema introspection for building UIs dynamically:

Entity-Specific:

  • GET /api/v1/{plural}/metadata - Complete metadata overview
  • GET /api/v1/{plural}/metadata/type - Type-specific metadata
  • GET /api/v1/{plural}/metadata/fields - All field metadata
  • GET /api/v1/{plural}/metadata/field/[name] - Individual field

Global:

  • GET /api/v1/schema/metadata - All entities with complete metadata
  • GET /api/v1/schema/metadata/list - Lightweight entity list
  • GET /api/v1/schema/metadata/{entity} - Specific entity metadata

See ../datalayer/metadata.md for complete metadata API reference.

Self-Contained Packages

Each generated datalayer is a self-contained npm package:

Location: .radish/lib/datalayer/package.json

Includes:

  • All necessary dependencies (mongoose, zod, bcrypt, etc.)
  • Independent versioning
  • Can be installed via file: dependency or published to registry

Why Self-Contained:

  • No dependency conflicts with main app
  • Generated code has explicit dependencies
  • Can be versioned and published independently
  • Works in monorepo or standalone

Architecture Principles

Services-First:

  • Application code should use services, not repos
  • Services enforce auth, validation, and business rules
  • Repos are implementation details

Database-Agnostic Abstractions:

  • Service layer doesn't know about Mongoose/Prisma
  • Population config is database-agnostic
  • Easy to swap database adapters

Consistent Error Handling:

  • All errors follow standard envelope format
  • HTTP adapters map error codes to status codes
  • Field-level validation details included

Permission-First:

  • Every operation checks permissions
  • Owner scoping applied automatically
  • Granular per-operation permissions

Next Steps