Skip to content

Architecture

entrest is an entc extension. You register it in entc.go with entc.Extensions(entrest.NewExtension(...)). When you run go generate, entrest runs alongside Ent's own codegen and writes artifacts under your Ent target directory (typically <ent>/rest/).

entrest spec generation diagram

At a high level, generation happens in three phases: prepare the graph, build and merge the OpenAPI spec, then emit handler templates.

flowchart TD
    Schema[Ent schemas + entrest annotations] --> EntC[entc.LoadGraph]
    EntC --> Graph[gen.Graph]
    Graph --> Validate[ValidateAnnotations]
    Validate --> Base[Load base spec]
    Base --> PreHook[PreGenerateHook]
    PreHook --> Fragments[Per-type spec fragments]
    Fragments --> Merge[MergeSpecOverlap]
    Merge --> PostHook[PostGenerateHook]
    PostHook --> Globals[Global headers + error responses]
    Globals --> PreWrite[PreWriteHook]
    PreWrite --> Write[Write openapi.json]
    Write --> Templates[Generate rest/*.go templates]

Ent loads your schema files into a *gen.Graph. entrest registers two entc hooks:

  1. JSON tag patch (unless DisablePatchJSONTag is set) — removes omitempty from generated struct tags so default-valued fields still appear in JSON responses.
  2. Spec and handler generation — the main Generate pass described below.

Before any paths are built, ValidateAnnotations checks that each entrest annotation is attached at the correct level (schema, field, or edge). Misplaced annotations fail codegen immediately with a message naming the field and allowed locations.

For each schema node in the graph:

  1. Skip schemas (or edges) marked with WithSkip or with zero allowed operations.
  2. Entity operations — for each allowed CRUD operation (create, read, update, delete, list), build a spec fragment via GetSpecType. Schemas without an explicit id field only get list and create.
  3. Edge endpoints — for edges with an ID (composite/through edges are skipped), generate list or read fragments via GetSpecEdge when WithEdgeEndpoint allows it.
  4. Unless DisableSpecHandler is set, append a GET /openapi.json meta endpoint.

Each fragment is a small ogen.Spec containing paths and components for one operation. All fragments are merged into a base spec using overlap merge (MergeSpecOverlap): paths with the same URL combine HTTP methods; component map keys from later fragments win on conflict.

The merge target is chosen in this order:

SourceConfig field
In-memory specConfig.Spec
JSON file on diskConfig.SpecFromPath
Empty specDefault openapi: 3.0.3, info.title: EntGo Rest API, info.version: 1.0.0

You cannot set both Spec and SpecFromPath. See Extending the Spec for adding custom paths, security schemes, and metadata.

These run after PostGenerateHook and before the spec is written:

Custom paths added in PreGenerateHook receive globals automatically. Paths added in PostGenerateHook also receive them because globals are applied after that hook.

PreWriteHook runs last — after globals — and receives only the finished *ogen.Spec (no graph access).

The final document is written to <ent-target>/rest/openapi.json unless you set a custom Writer.

When Config.Handler is not HandlerNone, entc renders Go templates into <ent-target>/rest/:

FilePurpose
server.goNewServer, route registration, /openapi.json, /docs
create.go, list.go, update.go, …Per-operation handler functions
sorting.go, eagerload.go, optional.goQuery helpers

WithTesting adds request helpers under <ent-target>/enttest/. HandlerNone generates only the spec — no handler package.

entrest uses two configuration layers that resolve at codegen time:

flowchart LR
    Config[entrest.Config in entc.go] --> Extension[entc extension]
    Annotations[entrest annotations on schema/field/edge] --> Graph
    Extension --> Graph
    Graph --> Spec[openapi.json]
    Graph --> Handler[rest package]

Passed to entrest.NewExtension(&entrest.Config{...}) in entc.go. Controls defaults for every schema — pagination bounds, eager-load policy, default CRUD operations, handler router type (HandlerStdlib, HandlerChi, or HandlerNone), global headers/errors, hooks, and more.

See Codegen Configuration for the full field list.

Attached on schemas, fields, and edges via entrest.With...() in your Ent schema files. Examples: WithSkip, WithEagerLoad, WithFilter, WithPagination.

Precedence is generally: edge annotation → parent schema annotation → Config default. For example, WithExcludeOperations on an edge overrides the parent schema's operation set.

See Annotation Reference for every annotation and its allowed attachment point.

Separate from codegen. Configured when you call rest.NewServer(db, cfg) in main.go. Controls mount paths, body size limits, error masking, and docs/spec serving at runtime. The generated handler code is already fixed; ServerConfig only changes how the running server behaves.

See Runtime Configuration.

After go generate, a typical project contains:

internal/database/ent/
├── ... # standard Ent generated code
└── rest/
├── openapi.json # merged OpenAPI 3.0.3 spec
├── server.go # route registration + Server type
├── create.go
├── list.go
└── ...

The embedded openapi.json is also available as rest.OpenAPI bytes in the generated package. When spec serving is enabled, the handler exposes it at GET {BasePath}/openapi.json.

Generated REST paths follow consistent conventions:

PatternExample
List / create/users, /pets
Read / update / delete/users/{userID}, /pets/{petID}
Edge list/users/{userID}/pets
Edge read (unique)/pets/{petID}/owner

Entity segments use kebab-case plurals. Path parameters use camelCase entity name + ID (for example {userID}).

llms.txtdocumentation for LLMs