05 — API Surface
The Admin Console should expose a contract-driven API using Hono and Zod.
The API should be split into two broad surfaces:
text
Model Administration API
manages metadata and publishing
Runtime Entity API
creates and manipulates actual entities using published metadataAPI Principles
- Contracts live in
packages/contracts. - Backend routes import contracts and implement them.
- Frontend imports contracts and typed API clients.
- Runtime entity operations use the currently published model version.
- Draft metadata changes do not affect runtime CRUD until published.
- All model changes are audited.
Suggested Route Groups
text
/api/model-versions
/api/model/entity-types
/api/model/hierarchy-rules
/api/model/property-groups
/api/model/property-definitions
/api/model/tag-groups
/api/model/tags
/api/model/relationship-definitions
/api/model/templates
/api/model/validation
/api/model/publish
/api/entities
/api/entities/:entityId
/api/entities/:entityId/properties
/api/entities/:entityId/tags
/api/entities/:entityId/relationships
/api/entities/:entityId/children
/api/entities/:entityId/descendants
/api/entities/:entityId/ancestors
/api/entities/:entityId/pathModel Version Endpoints
text
GET /api/model-versions
POST /api/model-versions/draft
GET /api/model-versions/:id
POST /api/model-versions/:id/validate
POST /api/model-versions/:id/publish
POST /api/model-versions/:id/archiveEntity Type Endpoints
text
GET /api/model/entity-types?modelVersionId=...
POST /api/model/entity-types
GET /api/model/entity-types/:id
PATCH /api/model/entity-types/:id
DELETE /api/model/entity-types/:idProperty Definition Endpoints
text
GET /api/model/property-definitions?modelVersionId=...
POST /api/model/property-definitions
GET /api/model/property-definitions/:id
PATCH /api/model/property-definitions/:id
DELETE /api/model/property-definitions/:idRelationship Definition Endpoints
text
GET /api/model/relationship-definitions?modelVersionId=...
POST /api/model/relationship-definitions
GET /api/model/relationship-definitions/:id
PATCH /api/model/relationship-definitions/:id
DELETE /api/model/relationship-definitions/:idRuntime Entity Endpoints
text
POST /api/entities
GET /api/entities/:entityId
PATCH /api/entities/:entityId
DELETE /api/entities/:entityId
POST /api/entities/:entityId/restore
POST /api/entities/:entityId/purgeRuntime Property Endpoints
text
GET /api/entities/:entityId/properties
PUT /api/entities/:entityId/properties/:propertyKey
DELETE /api/entities/:entityId/properties/:propertyKeyRuntime Relationship Endpoints
text
GET /api/entities/:entityId/relationships
POST /api/entities/:entityId/relationships
DELETE /api/entities/:entityId/relationships/:relationshipIdExample Contract Shape
ts
import { z } from "zod";
export const createEntityTypeInputSchema = z.object({
modelVersionId: z.string().uuid(),
key: z.string().min(1).regex(/^[a-z][a-z0-9_]*$/),
name: z.string().min(1),
description: z.string().optional(),
icon: z.string().optional(),
isRootAllowed: z.boolean().default(false),
isHierarchyAllowed: z.boolean().default(true),
canHaveChildren: z.boolean().default(true),
});
export const entityTypeDtoSchema = createEntityTypeInputSchema.extend({
id: z.string().uuid(),
createdAt: z.string(),
updatedAt: z.string(),
deletedAt: z.string().nullable(),
});Generic Runtime Entity Create Input
ts
export const createRuntimeEntityInputSchema = z.object({
entityTypeKey: z.string(),
parentEntityId: z.string().uuid().nullable().optional(),
templateKey: z.string().optional(),
properties: z.record(z.string(), z.unknown()).default({}),
tags: z.array(z.string()).default([]),
});The backend must resolve this input against the published model before creating anything.
API Validation Layers
Validation should happen in layers:
- Zod validates shape.
- Model rules validate keys and allowed combinations.
- Permission checks validate actor capability and root scope.
- Service invariants validate lifecycle and consistency.
- Database constraints validate referential integrity.
Error Format
Use a consistent error shape:
ts
export const apiErrorSchema = z.object({
code: z.string(),
message: z.string(),
details: z.unknown().optional(),
fieldErrors: z.record(z.string(), z.array(z.string())).optional(),
});Example codes:
text
MODEL_VALIDATION_FAILED
ENTITY_TYPE_NOT_FOUND
PROPERTY_NOT_ALLOWED_FOR_TYPE
RELATIONSHIP_NOT_ALLOWED
ENTITY_OUTSIDE_ROOT_SCOPE
PUBLISHED_MODEL_IMMUTABLE
DRAFT_REQUIRED
DESTRUCTIVE_CHANGE_REQUIRES_APPROVAL