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
- Blueprint Definition - Define entities, fields, and relationships in YAML
- Schema Validation - Validate against JSON Schema using @radish/schemas
- Code Generation - Generate contracts, models, repos, services, and routes
- 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
reffields are indexed - All fields in
filtersarray 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 recordfindById(id)- Find by IDlist(criteria)- List with filtering, pagination, sortingupdate(id, data)- Update recordarchive(id)/restore(id)- Soft delete operationsremove(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 entitiesPOST /api/v1/{plural}- Create entityGET /api/v1/{plural}/[id]- Get entityPUT /api/v1/{plural}/[id]- Update entityDELETE /api/v1/{plural}/[id]- Delete entityPOST /api/v1/{plural}/bulk- Bulk create with validationGET /api/v1/{plural}/metadata- Entity metadataGET /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
- Client sends
POST /api/v1/productswith JSON body - Route Handler extracts body and calls
ProductService.create() - Service validates input with
CreateProductZod schema - Service checks permissions (
product:create:ownorproduct:create:all) - Service applies owner scoping (sets
ownerIdto current user) - Service resolves macros (e.g.,
{{user.name}}→ actual name) - Repository creates the database record
- Response returns standardized success envelope with created product
Base Entity Fields
All entities automatically include:
_id(orid) - Primary keyownerId- Reference to owning usercreatedAt- Timestamp of creationupdatedAt- Timestamp of last updatearchivedAt- 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:
archivedAtfield: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 deleterestore(id)- Restore from archivelistArchived(criteria)- Query archived itemsremove(id)- Hard delete (permanent, use with caution)
Service Methods:
archive(id)- With permission checkrestore(id)- With permission checkarchiveCascade(id)- Archive entity and all dependent entitiesrestoreCascade(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 failedUNAUTHORIZED(401) - Authentication requiredFORBIDDEN(403) - Permission deniedNOT_FOUND(404) - Resource not foundCONFLICT(409) - Resource conflict (e.g., duplicate)UNPROCESSABLE_ENTITY(422) - Business rule violationINTERNAL_ERROR(500) - Server errorSERVICE_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
_idsort
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 (
ownvsall) - Wildcard support (
product:*,*:read:all)
Database Indexes
Radish automatically generates optimal indexes:
Automatic Indexing:
- All
reffields (foreign keys) - All fields in
filtersarray - Compound indexes from blueprint
- 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.tsexportsEntityModel - Repositories:
entity.repository.tsexportsEntityRepository - Services:
entity.service.tsexportsEntityService - 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 overviewGET /api/v1/{plural}/metadata/type- Type-specific metadataGET /api/v1/{plural}/metadata/fields- All field metadataGET /api/v1/{plural}/metadata/field/[name]- Individual field
Global:
GET /api/v1/schema/metadata- All entities with complete metadataGET /api/v1/schema/metadata/list- Lightweight entity listGET /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
- Generated Code Overview: ../datalayer/overview.md
- Access Control: ../datalayer/access-control.md
- Versioning & Audit Trails: ../datalayer/versioning.md
- API Reference: ../datalayer/routes.md