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/).

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:
- JSON tag patch (unless
DisablePatchJSONTagis set) — removesomitemptyfrom generated struct tags so default-valued fields still appear in JSON responses. - Spec and handler generation — the main
Generatepass 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:
- Skip schemas (or edges) marked with
WithSkipor with zero allowed operations. - Entity operations — for each allowed CRUD operation (
create,read,update,delete,list), build a spec fragment viaGetSpecType. Schemas without an explicitidfield only getlistandcreate. - Edge endpoints — for edges with an ID (composite/through edges are skipped), generate list or
read fragments via
GetSpecEdgewhenWithEdgeEndpointallows it. - Unless
DisableSpecHandleris set, append aGET /openapi.jsonmeta 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:
| Source | Config field |
|---|---|
| In-memory spec | Config.Spec |
| JSON file on disk | Config.SpecFromPath |
| Empty spec | Default 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/:
| File | Purpose |
|---|---|
server.go | NewServer, route registration, /openapi.json, /docs |
create.go, list.go, update.go, … | Per-operation handler functions |
sorting.go, eagerload.go, optional.go | Query 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.
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:
| Pattern | Example |
|---|---|
| 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}).
- Codegen Configuration —
entrest.Configfields - Extending the Spec — base spec, hooks, security schemes
- Annotation Reference — per-schema overrides
- Mount the Handler — wiring the generated
restpackage - Troubleshooting — common codegen and runtime errors

