Skip to content

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 metadata

API Principles

  1. Contracts live in packages/contracts.
  2. Backend routes import contracts and implement them.
  3. Frontend imports contracts and typed API clients.
  4. Runtime entity operations use the currently published model version.
  5. Draft metadata changes do not affect runtime CRUD until published.
  6. 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/path

Model 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/archive

Entity 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/:id

Property 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/:id

Relationship 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/:id

Runtime 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/purge

Runtime Property Endpoints

text
GET    /api/entities/:entityId/properties
PUT    /api/entities/:entityId/properties/:propertyKey
DELETE /api/entities/:entityId/properties/:propertyKey

Runtime Relationship Endpoints

text
GET    /api/entities/:entityId/relationships
POST   /api/entities/:entityId/relationships
DELETE /api/entities/:entityId/relationships/:relationshipId

Example 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:

  1. Zod validates shape.
  2. Model rules validate keys and allowed combinations.
  3. Permission checks validate actor capability and root scope.
  4. Service invariants validate lifecycle and consistency.
  5. 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