Skip to content

Best Practices

entrest generates OpenAPI 3.0.3 specs and validates them during its own test suite. The patterns below are what the generator targets — they align with common linter rules (including IBM OpenAPI Validator) and with tools that generate HTTP clients from a spec.

Generated specs are checked with libopenapi-validator before entrest merges changes. You can run the same class of checks on your project's openapi.json with IBM OpenAPI Validator or Spectral. See Recommended Tools.

If validation fails after customizing the spec, the failure is usually a duplicate operationId, a conflicting path merge, or an invalid $ref.

entrest puts shared pieces under components and references them with $ref:

ComponentPurpose
PagedResponseShared pagination metadata (page, last_page, is_last_page, total_count)
Page, PrettyResponse, FilterOperationShared query parameters
ErrorBadRequest, ErrorNotFound, …Shared error responses
{Entity}TypeEnum, etc.Hoisted enum schemas
{Entity}Create, {Entity}Update, {Entity}ReadPer-operation body schemas

Enum fields are hoisted into component schemas (for example UserTypeEnum) so the same enum is not inlined on every property and filter parameter.

Global request headers are attached once per path via components/parameters, not repeated on every operation. Global response headers and error responses use components/headers and components/responses the same way.

When you extend the spec with Extending the Spec, prefer adding reusable components and $refing them rather than copying JSON into each path.

Each generated operation includes:

  • operationId — Unique across the whole spec. Default pattern: createUser, getUser, updateUser, deleteUser, listUsers, listUserPets, etc. Override with WithOperationID when you need a stable name for client codegen.
  • tags — One tag per entity (Users, Pets, …). Edge endpoints include both parent and child tags. AddEdgesToTags optionally adds eager-loaded entity names to parent operations.
  • summary and description — Defaults are generated from the entity name; override with WithOperationSummary and WithOperationDescription.
  • Path parameters{userID} style names (camelCase entity name + ID), defined in components/parameters and referenced on the path item.

Paths use plural kebab-case segments (/users, /users/{userID}/pets).

Create, update, and read use different component schemas (UserCreate, UserUpdate, UserRead). That keeps required fields and writable properties accurate for client generators — a create body is not the same as a response object.

Read responses use User plus optional UserEdges composed with allOf when edges are eager-loaded. List responses reference UserRead (or a paged wrapper around it).

Where ent (or your annotations) provide the data, the spec includes:

SourceOpenAPI field
field.Comment() or WithDescriptiondescription
WithExampleexample
Ent field Default()default
Go integer width (int8, uint32, …)minimum / maximum on integers
Optional() + Nillable()nullable: true
Ent enum valuesenum (often via a hoisted component schema)

Add examples and descriptions on fields that clients or docs viewers will display often. Passwords, tokens, and fields marked Sensitive() are omitted from read schemas; use WithSkip or WithReadOnly to keep fields out of write schemas too.

JSON fields without a native OpenAPI type need WithSchema (for example SchemaObjectAny for arbitrary objects).

List endpoints document page, per_page, sort, and order with defaults and bounds from Configuration. Paginated list responses use allOf with PagedResponse and a content array — see Pagination.

Filter query parameters are defined in components/parameters and referenced on the operation, with descriptions derived from the filter predicate (for example “Filters field email to equal the provided value”).

By default, every operation documents shared error responses for 400, 401, 403, 404, 409, 429, and 500. Each points at a component response whose body matches the shape returned by the generated HTTP handler:

{
"error": "",
"type": "Not Found",
"code": 404,
"request_id": "",
"timestamp": "2024-04-26T12:19:01Z"
}

type, code, and timestamp are always present; request_id appears when request ID middleware is configured.

entrest skips some status codes where they do not apply:

StatusOmitted when
404 on listListNotFound is false (default). Empty filter results return 200 with an empty content array.
409Operation is not create or update.

Customize the set with GlobalErrorResponses or extend bodies in Extending the Spec.

Tools like OpenAPI Generator, oapi-codegen, and Speakeasy rely on:

  1. Stable, unique operationId values — do not hand-edit generated IDs; use annotations if a generator chokes on a default name.
  2. Tags — group operations in generated clients; keep tag names aligned with your API surface.
  3. Explicit component schemas — entrest avoids anonymous inline objects on success responses where possible.
  4. Consistent error models — one error schema per status code, referenced everywhere.

If you only need the spec and not the handler, set Handler to HandlerNone and feed openapi.json directly into your generator.

Large Ent graphs produce large specs. Most of the bulk is filter parameters and eager-loaded edge schemas.

ApproachEffect
Eager-load only edges callers needFewer nested schemas in read/list responses. See Eager Loading.
DisableEagerLoadedEndpointsDrops redundant edge paths when the edge is already on the parent response.
DisableEdgeEndpoints or per-edge WithEdgeEndpointFewer paths and operations.
Narrow WithFilter usageFewer query parameters per list endpoint.
WithExcludeOperationsDrop unused CRUD operations for internal entities.
Leave AddEdgesToTags disabledLess tag noise on operations (edge endpoints still get both tags).
DisablePagination on small, bounded listsSimpler list response schemas (plain arrays instead of paged wrappers).

Enum hoisting and shared pagination/error components already reduce duplication; the items above reduce how much API surface is generated in the first place.

internal/database/schema/user.go
field.Enum("type").
NamedValues("System", "SYSTEM", "User", "USER").
Default("USER").
Annotations(
entrest.WithExample("USER"),
entrest.WithFilter(entrest.FilterGroupEqualExact | entrest.FilterGroupArray),
).
Comment("Account kind: SYSTEM for internal actors, USER for normal accounts.")

This yields a UserTypeEnum component, $ref usage on create/update/read schemas and filter parameters, default and example on the enum, and the comment as description.

llms.txtdocumentation for LLMs