# Calling Your New API (https://entrest.dev/guides/calling-your-new-api)
> **Tutorial**
>
> Continues [Getting Started](https://entrest.dev/guides/getting-started) — open the embedded API docs and try
> sample requests against a running server.
Now that you've got a basic HTTP API setup, you can start making API requests to it.
### API Documentation [#api-documentation]
You should now be able to access the API docs at [http://localhost:8080/docs](http://localhost:8080/docs),
which should display a nice UI ([scalar](https://github.com/scalar/scalar)) for exploring the API, and
executing sample requests:

### Sample Requests with Curl [#sample-requests-with-curl]
Here are a few sample requests that you can execute with [curl](https://curl.se/).
##### Create a new user (note the `id` field in the response, we'll use that in further requests) [#create-a-new-user-note-the-id-field-in-the-response-well-use-that-in-further-requests]
```bash
curl --request POST \
--url 'http://localhost:8080/users?pretty=true' \
--header 'Content-Type: application/json' \
--data '{
"username": "lrstanley",
"display_name": "Liam Stanley",
"email": "lrstanley@example.com"
}'
```
```json
{
"id": 4294967297,
"username": "lrstanley",
"display_name": "Liam Stanley",
"email": "lrstanley@example.com",
"edges": {}
}
```
##### Create a new pet who is owned by the user we just created [#create-a-new-pet-who-is-owned-by-the-user-we-just-created]
```bash /4294967297/
curl --request POST \
--url 'http://localhost:8080/pets?pretty=true' \
--header 'Content-Type: application/json' \
--data '{
"name": "Riley",
"age": 2,
"type": "DOG",
"owner": 4294967297
}'
```
```json
{
"id": 1,
"name": "Riley",
"age": 2,
"type": "DOG",
"edges": {
"owner": {
"id": 4294967297,
"username": "lrstanley",
"display_name": "Liam Stanley",
"email": "lrstanley@example.com",
"edges": {}
}
}
}
```
Notice that the response includes the `edges` field. This is because we've added the `owner` edge to
be eager-loaded in the schema, as shown below:
```go title="internal/database/schema/pet.go" {9}
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
// [...]
edge.From("owner", User.Type).
Ref("pets").
Unique().
Comment("The user that owns the pet.").
Annotations(
entrest.WithEagerLoad(true),
entrest.WithFilter(entrest.FilterEdge),
),
// [...]
}
}
```
##### Now query the pets that the user owns [#now-query-the-pets-that-the-user-owns]
This uses the auto-generated edge endpoint on the `User` schema.
```bash /4294967297/
curl --request GET \
--url 'http://localhost:8080/users/4294967297/pets?pretty=true'
```
```json
{
"page": 1,
"total_count": 1,
"last_page": 1,
"is_last_page": true,
"content": [
{
"id": 1,
"name": "Riley",
"age": 2,
"type": "DOG",
"edges": {
"owner": {
"id": 4294967297,
"username": "lrstanley",
"display_name": "Liam Stanley",
"email": "lrstanley@example.com",
"edges": {}
}
}
}
]
}
```
# Getting Started (https://entrest.dev/guides/getting-started)
> **Tutorial**
>
> Step-by-step setup for a new entrest project — schemas, codegen, and a running HTTP server. Assumes
> basic familiarity with [EntGo](https://entgo.io/).
**entrest** is an [EntGo](https://entgo.io/) extension for generating compliant OpenAPI
specs and an HTTP handler implementation that matches that spec. It expands upon the
approach used by [entoas](https://github.com/ent/contrib/tree/master/entoas#entoas),
with additional functionality, and pairs the generated specification with a
fully-functional HTTP handler implementation.
- ✨ Generates OpenAPI specs for your EntGo schema.
- ✨ Generates a fully functional HTTP handler implementation that matches the OpenAPI spec.
- ✨ Supports automatic pagination (where applicable).
- ✨ Supports advanced filtering (using query parameters, `AND`/`OR` predicates, etc).
- ✨ Supports eager-loading edges, so you don't have to make additional calls unnecessarily.
- ✨ Supports various forms of sorting.
- ✨ And more!
> **Tip**
>
> It is recommended to read through the [EntGo getting started guide](https://entgo.io/docs/getting-started/)
> before proceeding to get a better understanding of EntGo.
>
> Additionally, to load extensions with additional configuration, using EntGo's
> [entc integration](https://entgo.io/docs/code-gen/#use-entc-as-a-package) is required.
## Project Structure [#project-structure]
We recommend a project structure that looks similar to this:
```text
├── internal
│ ├── ...
│ ├── database
│ │ ├── ent/ # contains all entgo-generated code
│ │ │ ├── ...
│ │ │ └── rest/ # HTTP handler, OpenAPI spec & entrest code
│ │ ├── schema/ # entgo schemas (w/ entrest annotations)
│ │ │ ├── ...
│ │ │ ├── schema_foo.go
│ │ │ └── schema_bar.go
│ │ └── entc.go # entc logic (where to load entrest)
│ └── generate.go # used to trigger all internal codegen logic.
└── main.go # main entrypoint for the application, mount HTTP handler, custom endpoints, etc
```
##### Why? [#why]
There are a few reasons why we recommend this structure:
- Generated code is isolated in its own folder. In most cases, you can simply ignore the folder in your
editor, so it's less distracting.
- Schemas aren't buried compared to the standard entgo setup.
- All database-related code is explicitly in an `internal/` folder, which is best practice for logic
that shouldn't be imported by external packages.
##### Future Growth [#future-growth]
As the complexity of your project grows, it may be worth separating out logic even further. Some examples:
- HTTP logic -- `main.go -> http.go`.
- Initializing the DB client/migrations -- `main.go -> internal/database/client.go`
- Multiple "main" entrypoints (API backend, CLI, worker(s), etc) -- `main.go -> cmd/http-server/main.go`
## Initialize Project [#initialize-project]
You can find a full example project that matches this guide [here](https://github.com/lrstanley/entrest/tree/master/_examples/simple).
1. Start by initializing a new Go project:
```bash
mkdir -p my-project && cd my-project
go mod init github.com/example/my-project
```
2. Setup subfolder for schema files:
```bash
mkdir -p internal/database/schema
```
3. Add a few schema files:
Example `Pet` schema, which also has a few edge relationships, one of which we eager load. Take
note of some of the attached annotations:
```go {7,19-21,27-29,42-44,56-57,62} title="internal/database/schema/pet.go"
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"github.com/lrstanley/entrest"
)
type Pet struct {
ent.Schema
}
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.Int("id"),
field.String("name").
Annotations(
entrest.WithExample("Kuro"),
entrest.WithSortable(true),
entrest.WithFilter(entrest.FilterGroupEqual|entrest.FilterGroupArray),
),
field.Int("age").
Min(0).
Max(50).
Annotations(
entrest.WithExample(2),
entrest.WithSortable(true),
entrest.WithFilter(entrest.FilterGroupEqualExact|entrest.FilterGroupArray|entrest.FilterGroupLength),
),
field.Enum("type").
NamedValues(
"Dog", "DOG",
"Cat", "CAT",
"Bird", "BIRD",
"Fish", "FISH",
"Amphibian", "AMPHIBIAN",
"Reptile", "REPTILE",
"Other", "OTHER",
).
Annotations(
entrest.WithExample("DOG"),
entrest.WithSortable(true),
entrest.WithFilter(entrest.FilterGroupEqualExact|entrest.FilterGroupArray),
),
}
}
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("pets").
Unique().
Comment("The user that owns the pet.").
Annotations(
entrest.WithEagerLoad(true),
entrest.WithFilter(entrest.FilterEdge),
),
edge.To("friends", Pet.Type).
Comment("Pets that this pet is friends with.").
Annotations(
entrest.WithFilter(entrest.FilterEdge),
),
}
}
```
Example `User` schema, which also has a single edge relationship, which we eager load. Take note
of some of of the attached annotations:
```go {8,21-22,27-28,35-37,48-49} title="internal/database/schema/user.go"
package schema
import (
"entgo.io/ent"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"github.com/lrstanley/entrest"
)
type User struct {
ent.Schema
}
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("username").
Unique().
Immutable().
Annotations(
entrest.WithSortable(true),
entrest.WithFilter(entrest.FilterGroupEqual|entrest.FilterGroupArray),
).
Comment("Username of the user."),
field.String("display_name").
Annotations(
entrest.WithSortable(true),
entrest.WithFilter(entrest.FilterGroupEqual|entrest.FilterGroupArray),
).
Comment("full name/display name of the user."),
field.String("email").
MinLen(1).
MaxLen(320).
Annotations(
entrest.WithSortable(true),
entrest.WithExample("John.Smith@example.com"),
entrest.WithFilter(entrest.FilterGroupEqual|entrest.FilterGroupArray),
).
Comment("Email associated with the user."),
}
}
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type).
Comment("Pets owned by the user.").
Annotations(
entrest.WithEagerLoad(true),
entrest.WithFilter(entrest.FilterEdge),
entsql.OnDelete(entsql.SetNull),
),
}
}
```
4. Add an `internal/database/entc.go` file, which is used to configure the extension, and run code
generation. Make sure to replace the `github.com/example/my-project`
module references with your module name.
```go {10,14-21,27-28,30} title="internal/database/entc.go"
//go:build ignore
package main
import (
"log"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/lrstanley/entrest"
)
func main() {
ex, err := entrest.NewExtension(&entrest.Config{
Handler: entrest.HandlerStdlib,
WithTesting: true,
StrictMutate: true,
})
if err != nil {
log.Fatalf("creating entrest extension: %v", err)
}
err = entc.Generate(
"./database/schema",
&gen.Config{
Target: "./database/ent",
Schema: "github.com/example/my-project/internal/database/schema",
Package: "github.com/example/my-project/internal/database/ent",
},
entc.Extensions(ex),
)
if err != nil {
log.Fatalf("failed to run ent codegen: %v", err)
}
}
```
5. Add a `internal/generate.go` file, where the `go:generate` comment is used to trigger all internal
codegen logic:
```go {3} title="internal/generate.go"
package internal
//go:generate go run -mod=readonly database/entc.go
```
6. Track the codegen dependencies as [Go tools](https://go.dev/doc/go1.24#tools) (Go 1.24+). Because
`entc.go` is excluded from normal builds (`//go:build ignore`), `go mod tidy` can't see what it
imports. Registering them as tools adds a `tool` block to your `go.mod`, so they're pinned and kept
up to date like any other dependency:
```bash
go get -tool entgo.io/ent/entc github.com/lrstanley/entrest
```
7. Add a `main.go` file, which mounts the HTTP handler, and initializes the database client:
```go {14,40,48} title="main.go"
//nolint:all
package main
import (
"context"
"database/sql"
"fmt"
"net/http"
"entgo.io/ent/dialect/sql/schema"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/example/my-project/internal/database/ent"
"github.com/example/my-project/internal/database/ent/rest"
_ "github.com/example/my-project/internal/database/ent/runtime" // Required by ent.
"modernc.org/sqlite"
)
func main() {
sql.Register("sqlite3", &sqlite.Driver{})
db, err := ent.Open("sqlite3", "file:local.db?cache=shared&_pragma=foreign_keys(1)&_busy_timeout=15")
if err != nil {
panic(err)
}
defer db.Close()
ctx := context.Background()
err = db.Schema.Create(
ctx,
schema.WithDropColumn(true),
schema.WithDropIndex(true),
schema.WithGlobalUniqueID(true),
schema.WithForeignKeys(true),
)
if err != nil {
panic(err)
}
srv, err := rest.NewServer(db, &rest.ServerConfig{})
if err != nil {
panic(err)
}
fmt.Println("running http server")
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Mount("/", srv.Handler())
http.ListenAndServe(":8080", r)
}
```
8. Lastly, run code generation:
```bash
go generate -x ./...
```
That's it! You should now have a fully-functional project that uses entrest. This includes:
1. A managed database schema using [EntGo](https://entgo.io/).
2. A fully-functional HTTP handler implementation using [chi](https://github.com/go-chi/chi) (or any
other HTTP router that supports `net/http`).
3. A fully-functional OpenAPI spec, which can be used to generate client libraries, or to validate
requests (see `internal/database/ent/rest/openapi.json`).
## Running The Server [#running-the-server]
To run the application, simply run the following commands to download all dependencies, then run the
application:
```bash
go mod tidy
go run main.go
```
```console
[...]
go: downloading github.com/go-playground/assert/v2 v2.0.1
[...]
running http server
```
## Next Steps [#next-steps]
Now that you've got a basic HTTP API setup, you can start making API requests to it, or further customizing
it.
- [Calling Your New API](https://entrest.dev/guides/calling-your-new-api): Learn how to view your API documentation, and make sample requests.
- [Annotation Reference](https://entrest.dev/openapi-specs/annotation-reference): Every annotation, and where it can be applied.
- [Mount the Handler](https://entrest.dev/http-handler/getting-started): Mount the generated HTTP handler and expose CRUD endpoints.
- [Runtime Configuration](https://entrest.dev/http-handler/configuration): Configure the generated HTTP handler via rest.ServerConfig.
# API Docs (https://entrest.dev/http-handler/api-docs)
> **How-to guide**
>
> This page shows how to customize or replace the embedded API reference UI. Runtime toggles
> (`DisableSpecHandler`, `DisableDocsHandler`, `BaseURL`, server URL injection, and so on) are
> documented in [Runtime Configuration](https://entrest.dev/http-handler/configuration).
When [`entrest.Config.DisableSpecHandler`](https://pkg.go.dev/github.com/lrstanley/entrest#Config) is **false**
at generation time, entrest embeds the OpenAPI spec in your generated `rest` package and registers
`GET {BasePath}/openapi.json` and `GET {BasePath}/docs` (an embedded [Scalar](https://github.com/scalar/scalar)
UI). With the stdlib handler, `GET /` (after `BasePath` is stripped) redirects to `/docs`.
See [Configuration — Base URL and path](https://entrest.dev/http-handler/configuration#base-url-and-path) for how mount paths
affect these URLs.

## Default embedded UI [#default-embedded-ui]
The `/docs` route is implemented by `Server.Docs`. It renders a single inline HTML document on each
request and loads Scalar from the jsDelivr CDN — there is no separate static site.
> **Note**
>
> There is no runtime option to tweak Scalar's theme or layout on the embedded `/docs` handler. To change
> the UI, disable the embedded docs route and serve your own page (see below).
Route registration, spec serving, and `DisableDocsHandler` are covered in
[Configuration — OpenAPI spec endpoint](https://entrest.dev/http-handler/configuration#openapi-spec-endpoint) and
[API reference docs](https://entrest.dev/http-handler/configuration#api-reference-docs).
## Replace the embedded UI [#replace-the-embedded-ui]
Any OpenAPI viewer that accepts a spec URL can replace the embedded Scalar page:
1. Set [`DisableDocsHandler`](https://entrest.dev/http-handler/configuration#api-reference-docs) to `true` so `/docs` is not
registered by the generated handler.
2. Keep `/openapi.json` enabled (unless you serve the spec from another origin).
3. Register your own `GET` handler that returns a single HTML document.
4. Point the viewer at `{BasePath}/openapi.json` (or an absolute URL if the UI is on another host).
The examples below use [RapiDoc](https://rapidocweb.com/) and mount custom docs at `/v1/docs`.
### Stdlib (`http.ServeMux`) [#stdlib-httpservemux]
This example serves one self-contained HTML page from Go with `fmt.Fprintf`, loads RapiDoc from the
unpkg CDN, and overrides component attributes for a focused dark layout.
```go title="main.go"
// [...]
func rapidocDocs(baseURL string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
uri := strings.TrimSuffix(baseURL, "/")
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
Pet Store API
`, uri+"/openapi.json")
}
}
func main() {
// [...]
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/docs{$}", rapidocDocs(cfg.BaseURL))
mux.Handle("/", srv.Handler())
log.Fatal(http.ListenAndServe(":8080", mux))
}
```
### Chi [#chi]
Register the docs route on the same sub-router where you mount the generated API. Set `BasePath` to
match the mount point so spec URLs stay correct — see
[Configuration — Stdlib vs chi](https://entrest.dev/http-handler/configuration#stdlib-vs-chi).
```go title="main.go"
cfg := &rest.ServerConfig{
BaseURL: "https://api.example.com/v1",
BasePath: "/v1",
DisableDocsHandler: true,
}
srv, err := rest.NewServer(db, cfg)
if err != nil {
panic(err)
}
r := chi.NewRouter()
r.Route("/v1", func(r chi.Router) {
r.Get("/docs", rapidocDocs(cfg.BaseURL))
srv.Handler(r)
})
http.ListenAndServe(":8080", r)
```
# Authentication & Permissions (https://entrest.dev/http-handler/auth-and-permissions)
> **How-to guide**
>
> entrest does not ship login flows, session stores, or role models. You authenticate requests in HTTP
> middleware, enforce access with [Ent privacy policies](https://entgo.io/docs/privacy/), and use
> generation-time annotations to limit what the API exposes.
## Overview [#overview]
A typical setup has two layers:
1. **Authentication** — HTTP middleware validates credentials (JWT, session cookie, API key, etc.) and
stores the caller's identity on the request context.
2. **Authorization** — Ent privacy policies read that context on every query and mutation the handler
runs.
The generated handler injects the `ent.Client` into the request context via `UseEntContext`. Privacy
policies run automatically on handler-driven database calls when the
[`privacy` feature](https://entgo.io/docs/privacy/) is enabled at codegen time.
```mermaid
flowchart LR
Client --> Middleware
Middleware -->|"identity on context"| Handler
Handler -->|"ent.NewContext"| Ent
Ent --> Privacy
Privacy --> DB[(database)]
```
Return **401 Unauthorized** from middleware when credentials are missing or invalid. Reserve
**403 Forbidden** for requests that are authenticated but not allowed by a privacy policy.
## Authentication middleware [#authentication-middleware]
Validate the request before it reaches the generated handler and attach whatever your privacy rules
need — user ID, roles, tenant ID, and so on.
```go title="auth/middleware.go"
type contextKey int
const userKey contextKey = iota
type User struct {
ID int
Role string
}
func UserFromContext(ctx context.Context) (*User, bool) {
u, ok := ctx.Value(userKey).(*User)
return u, ok
}
func WithUser(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if token == "" {
http.Error(w, "missing credentials", http.StatusUnauthorized)
return
}
user, err := validateToken(token)
if err != nil {
http.Error(w, "invalid credentials", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r.WithContext(WithUser(r.Context(), user)))
})
}
```
Mount middleware **outside** the generated handler so every route — including custom ones you add
alongside entrest — sees the same context:
```go title="main.go"
srv, err := rest.NewServer(db, nil)
if err != nil {
panic(err)
}
handler := AuthMiddleware(srv.Handler())
http.ListenAndServe(":8080", handler)
```
With chi, apply middleware on the router before calling `srv.Handler(r)` — see
[Configuration — Stdlib vs chi](https://entrest.dev/http-handler/configuration#stdlib-vs-chi).
You do not need to call `UseEntContext` yourself when using the generated handler — it wraps the mux
for you. Only register it separately if another handler needs the client on requests that never reach
`Server.Handler` — see [Configuration — Ent client context](https://entrest.dev/http-handler/configuration#ent-client-context).
## Authorization with privacy policies [#authorization-with-privacy-policies]
Enable the privacy feature at codegen time (`gen.FeaturePrivacy`) and implement `Policy()` on schemas
that need access control. Policy design — rules, filters, row-level scoping, role checks — is covered
in the [Ent privacy documentation](https://entgo.io/docs/privacy/).
From entrest's perspective, the important part is that policies read the same context values your
middleware sets. A minimal example: public reads, authenticated writes.
```go title="schema/schema_post_policy.go"
func (Post) Policy() ent.Policy {
return privacy.Policy{
Query: privacy.QueryPolicy{
privacy.AlwaysAllowRule(),
},
Mutation: privacy.MutationPolicy{
privacy.ContextQueryMutationRule(func(ctx context.Context) error {
if _, ok := UserFromContext(ctx); ok {
// You could run additional queries here to validate permissions,
// check info from the context on what the users role may be,
// and/or use advanced privacy features (documented in the Ent
// privacy docs), which allow filtering down resources, (e.g.
// only showing public blog posts unless the user is an admin).
return privacy.Allow
}
return privacy.Skip
}),
privacy.AlwaysDenyRule(),
},
}
}
```
The handler does not know about your auth model — it runs Ent queries with the request context, and
privacy policies decide what is allowed.
> **Running internal ent queries when using privacy policies**
>
> Note that when adding privacy policies like this, typical backend non-user-driven queries will
> now begin to fail, because the context does not have the associated authenticated user information.
> As such, there are typically 2 approaches to resolve this:
>
> 1. Inject a privacy.Allow [decision context override](https://entgo.io/docs/privacy/#decision-context)
> into the context, which the Ent privacy layer will honor.
> 2. Create a `SYSTEM` user (as an example), which is considered the author of all system-driven queries,
> and is what becomes associated in your database (if you use the invoked user as authors or similar).
## Handler error mapping [#handler-error-mapping]
When a privacy rule returns `privacy.Deny`, the generated `DefaultErrorHandler` responds with
**403 Forbidden** and the standard error envelope. See
[Configuration — Error handling](https://entrest.dev/http-handler/configuration#error-handling).
| Source | Typical status |
| --- | --- |
| Auth middleware — missing or invalid credentials | 401 |
| Privacy policy — `privacy.Deny` | 403 |
| Row not found after policy filtering | 404 |
Set `MaskErrors` in production so internal policy messages do not leak to clients.
## Limit what the API exposes [#limit-what-the-api-exposes]
Privacy policies run at the database layer. Annotations and Ent field options shrink the attack
surface at generation time.
Mark secrets with Ent's `Sensitive()` so they are omitted from JSON responses and the OpenAPI spec.
Use [`entrest.WithSkip`](https://entrest.dev/openapi-specs/annotation-reference#withskip) when a field should never
appear in the REST API but is not marked sensitive.
[`entrest.WithReadOnly`](https://entrest.dev/openapi-specs/annotation-reference#withreadonly) keeps a field out of
create/update bodies. [`entrest.WithIncludeOperations`](https://entrest.dev/openapi-specs/annotation-reference#withincludeoperations)
and [`entrest.WithExcludeOperations`](https://entrest.dev/openapi-specs/annotation-reference#withexcludeoperations)
control which CRUD endpoints exist — useful for admin-only schemas or edges clients should not
reassign:
```go title="schema/schema_post.go"
edge.From("author", User.Type).
Ref("posts").
Unique().
Required().
Annotations(
entrest.WithExcludeOperations(
entrest.OperationCreate,
entrest.OperationUpdate,
),
)
```
Set the author in an Ent hook or default instead of letting clients pick any user ID.
[`entrest.WithAllowClientIDs`](https://entrest.dev/openapi-specs/annotation-reference#withallowclientids) and
[`entrest.Config.AllowClientIDs`](https://pkg.go.dev/github.com/lrstanley/entrest#Config) let
clients send an `id` on create. Only enable this with validation — a deleted resource recreated
with the same ID can otherwise be hijacked.
## Document security in OpenAPI [#document-security-in-openapi]
Authentication headers belong in OpenAPI **security schemes**, not
[`GlobalRequestHeaders`](https://pkg.go.dev/github.com/lrstanley/entrest#Config). Add schemes through
a base spec via [`SpecFromPath`](https://entrest.dev/openapi-specs/extending#specfrompath) or
[`Spec`](https://entrest.dev/openapi-specs/extending#spec-configuration-option):
```go title="base-openapi.json"
{
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"security": [{ "bearerAuth": [] }]
}
```
entrest merges this into the generated spec. [`DefaultErrorResponses`](https://pkg.go.dev/github.com/lrstanley/entrest#DefaultErrorResponses)
already documents **401** and **403** on operations. The embedded `/docs` UI supports
`persistAuth` when you use the default Scalar handler — see [API Docs](https://entrest.dev/http-handler/api-docs).
Declaring a scheme in the spec does not enforce it. Middleware and privacy policies do.
## Background and internal calls [#background-and-internal-calls]
Privacy policies also apply to code that never goes through HTTP — migrations, workers, startup
seeding. Use `privacy.DecisionContext(ctx, privacy.Allow)` to bypass checks for trusted internal
work. See the [Ent privacy docs](https://entgo.io/docs/privacy/#context-decision).
## Testing [#testing]
Wrap the handler the same way production does, then seed context through middleware or call the API
with credentials:
```go title="handler_test.go"
func authAs(user *User, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r.WithContext(WithUser(r.Context(), user)))
})
}
func TestCreatePostRequiresAuth(t *testing.T) {
ctx := context.Background()
db := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_pragma=foreign_keys(1)")
t.Cleanup(func() { db.Close() })
srv, err := rest.NewServer(db, nil)
require.NoError(t, err)
ts := enttest.WithExisting(t, authAs(&User{ID: 1, Role: "admin"}, srv.Handler()))
resp := ts.Request[ent.Post](ctx, http.MethodPost, "/posts", map[string]any{
"title": "hello world",
"body": "content here",
}).Must(t)
assert.Equal(t, http.StatusCreated, resp.Data.Code)
}
```
To assert a denial, skip `.Must(t)` and check `resp.Error.Code` — see
[Testing — Success and failure](https://entrest.dev/http-handler/testing#success-and-failure).
## Related [#related]
- [Ent privacy](https://entgo.io/docs/privacy/): Policy design, rules, and filters.
- [Runtime Configuration](https://entrest.dev/http-handler/configuration): Error handling, UseEntContext, and chi mounts.
- [Testing](https://entrest.dev/http-handler/testing): enttest request helpers.
- [Annotation reference](https://entrest.dev/openapi-specs/annotation-reference): WithSkip, operations, and read-only fields.
- [Extending the spec](https://entrest.dev/openapi-specs/extending): Base OpenAPI spec and security schemes.
# Runtime Configuration (https://entrest.dev/http-handler/configuration)
> **Reference**
>
> Runtime options for `rest.ServerConfig` when calling `rest.NewServer`. The authoritative field list
> lives in your generated `/rest` package (`server.go`).
The generated HTTP handler is configured via `rest.ServerConfig` when calling `rest.NewServer`.
These types are generated into your project's `/rest` package alongside the handler itself.
Passing `nil` is equivalent to `&rest.ServerConfig{}` — every field has a safe default.
> **Note**
>
> This page covers **runtime** options. Generation-time options — which router to generate, pagination
> defaults, strict mutation, embedding the OpenAPI spec, and so on — live on
> [`entrest.Config`](https://pkg.go.dev/github.com/lrstanley/entrest#Config) and are applied when you
> run `go generate`.
## Base URL and path [#base-url-and-path]
`BaseURL` and `BasePath` tell the handler where it is mounted. That information is used to:
- Strip the path prefix from incoming requests (stdlib handler only).
- Inject a `servers` entry into `/openapi.json`.
- Point the embedded `/docs` UI at the spec.
- Prefix `Link` response headers when [Link headers](#link-headers) are enabled.
If you set `BaseURL`, `BasePath` is inferred from the URL path when it is empty. A leading slash is
added if missing, and a trailing slash is stripped. An invalid `BaseURL` causes `NewServer` to
return an error.
```go title="main.go"
srv, err := rest.NewServer(db, &rest.ServerConfig{
BaseURL: "https://api.example.com/v1",
})
if err != nil {
panic(err)
}
http.ListenAndServe(":8080", srv.Handler())
```
With that config, `GET /v1/users` is routed to the generated `/users` endpoint, the spec advertises
`https://api.example.com/v1` as its server URL, and `/docs` loads `/v1/openapi.json`.
You can set `BasePath` alone when you do not want a server URL injected into the spec:
```go title="main.go"
srv, err := rest.NewServer(db, &rest.ServerConfig{
BasePath: "/v1",
})
```
If `BaseURL` is empty, `DisableSpecInjectServer` is forced on — there is no server URL to inject.
### Stdlib vs chi [#stdlib-vs-chi]
The generated `Handler` signature depends on [`entrest.Config.Handler`](https://pkg.go.dev/github.com/lrstanley/entrest#Config)
at generation time:
- **stdlib** (`entrest.HandlerStdlib`): `Handler() http.Handler`. Serve the returned handler at the
process root. `http.StripPrefix` applies `BasePath` for you.
- **chi** (`entrest.HandlerChi`): `Handler(r chi.Router)`. Mount the generated routes onto a router
(or a sub-router) yourself, and set `BasePath` to match that mount point so spec URLs and `Link`
headers stay correct.
```go title="chi"
cfg := &rest.ServerConfig{BasePath: "/v1"}
srv, err := rest.NewServer(db, cfg)
if err != nil {
panic(err)
}
r := chi.NewRouter()
r.Route("/v1", func(r chi.Router) {
srv.Handler(r)
})
http.ListenAndServe(":8080", r)
```
Do not also wrap the stdlib handler in a second prefix mount — that would strip `/v1` twice.
## Request body size [#request-body-size]
By default, every request handled by `Server.Handler` is subject to a maximum body size of **8 MiB**
(`DefaultMaxRequestBodyBytes`). This helps protect your API from oversized payloads and accidental
denial-of-service from large uploads.
The limit is enforced automatically — you do not need to register any middleware yourself when using the
generated handler. Under the hood, `UseMaxBodyBytes` is applied for you:
- If the client sends a `Content-Length` header larger than the limit, the request is rejected immediately
with **413 Request Entity Too Large** before the body is read.
- Otherwise, the request body is wrapped with [`http.MaxBytesReader`](https://pkg.go.dev/net/http#MaxBytesReader),
which stops reading once the limit is exceeded.
When a request exceeds the limit, the response uses the standard entrest error shape:
```json
{
"error": "http: request body too large",
"type": "Request Entity Too Large",
"code": 413,
"timestamp": "2026-08-20T12:00:00Z"
}
```
### Override the limit [#override-the-limit]
Set `ServerConfig.MaxRequestBodyBytes` to a positive byte count. A zero value keeps the default (8 MiB).
```go title="main.go"
srv, err := rest.NewServer(db, &rest.ServerConfig{
MaxRequestBodyBytes: 1 << 20, // 1 MiB
})
if err != nil {
panic(err)
}
http.ListenAndServe(":8080", srv.Handler())
```
You can also read the effective limit at runtime with `ServerConfig.GetMaxRequestBodyBytes()`.
### Disable the limit [#disable-the-limit]
Set `MaxRequestBodyBytes` to a **negative** value (for example, `-1`) to turn off body size checking entirely.
```go title="main.go"
srv, err := rest.NewServer(db, &rest.ServerConfig{
MaxRequestBodyBytes: -1,
})
if err != nil {
panic(err)
}
http.ListenAndServe(":8080", srv.Handler())
```
Disabling the limit removes the middleware wrapper from `Server.Handler`. If you assemble your own middleware
stack instead of using the generated handler, you can opt in with `UseMaxBodyBytes` directly:
```go title="custom router"
limit := rest.UseMaxBodyBytes(4 << 20) // 4 MiB
handler := limit(rest.UseEntContext(db)(mux))
```
> **Note**
>
> Multipart form parsing still uses `DefaultMaxRequestBodyBytes` as its in-memory cap when decoding request
> bodies via `Bind`, independent of whether you disabled the middleware limit on the server.
## OpenAPI spec endpoint [#openapi-spec-endpoint]
The handler serves the generated spec at `GET {BasePath}/openapi.json` unless you disable it.
Set `DisableSpecHandler` to skip registering that route. Doing so also disables the embedded
[API reference docs](#api-reference-docs), because they load the spec from that URL.
```go title="main.go"
srv, err := rest.NewServer(db, &rest.ServerConfig{
DisableSpecHandler: true,
})
```
When `BaseURL` is set, the handler injects it as a `servers` entry in the served spec so clients and
the docs UI know where to send requests. Injection is skipped if the spec already defines `servers`
(for example via [`Config.Spec`](https://pkg.go.dev/github.com/lrstanley/entrest#Config) /
`SpecFromPath`). Set `DisableSpecInjectServer` to always leave the spec untouched:
```go title="main.go"
srv, err := rest.NewServer(db, &rest.ServerConfig{
BaseURL: "https://api.example.com/v1",
DisableSpecInjectServer: true,
})
```
> **Note**
>
> `DisableSpecHandler`, `DisableSpecInjectServer`, `DisableDocsHandler`, `EnableLinks`, `BaseURL`, and
> `BasePath` are only generated when [`entrest.Config.DisableSpecHandler`](https://pkg.go.dev/github.com/lrstanley/entrest#Config)
> is **false** at generation time. Setting that generation option removes the spec from the binary
> entirely, not just the HTTP route.
## API reference docs [#api-reference-docs]
By default the handler also serves an embedded [Scalar](https://github.com/scalar/scalar) UI at
`GET {BasePath}/docs`. With the stdlib handler, a `GET` of `/` (after `BasePath` is stripped)
redirects to that page.
Set `DisableDocsHandler` to keep `/openapi.json` but skip the HTML UI — useful if you host docs
elsewhere:
```go title="main.go"
srv, err := rest.NewServer(db, &rest.ServerConfig{
DisableDocsHandler: true,
})
```
`DisableDocsHandler` has no effect when `DisableSpecHandler` is already true.
See [API Docs](https://entrest.dev/http-handler/api-docs) for replacing the embedded Scalar UI with a custom OpenAPI viewer.
## Link headers [#link-headers]
Set `EnableLinks` to add a [`Link`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link)
response header on successful (and error) responses. Clients can use it to discover the spec and
walk paginated list results without parsing the JSON body.
```go title="main.go"
srv, err := rest.NewServer(db, &rest.ServerConfig{
BaseURL: "https://api.example.com/v1",
EnableLinks: true,
})
```
When enabled, responses may include:
| Relation | When |
| --- | --- |
| `describedby`, `service-desc` | Spec handler is enabled — points at `{BasePath}/openapi.json` |
| `prev` | List operation, current page is greater than 1 |
| `next` | List operation, current page is not the last page |
Example header on a paginated list:
```http
Link: ; rel="describedby", ; rel="service-desc", ; rel="next"
```
See [Pagination](https://entrest.dev/openapi-specs/pagination) for the JSON pagination envelope these links
mirror.
## Error handling [#error-handling]
Every failed request is written as an `ErrorResponse`:
```json
{
"error": "ent: pet not found",
"type": "Not Found",
"code": 404,
"request_id": "abc123",
"timestamp": "2026-08-20T12:00:00Z"
}
```
`DefaultErrorHandler` maps errors to status codes:
| Condition | Status |
| --- | --- |
| Endpoint not found | 404 |
| Method not allowed | 405 |
| Bad request / invalid ID / validation error | 400 |
| Request body too large | 413 |
| Ent `privacy.Deny` (when the privacy feature is enabled) | 403 |
| Ent not found | 404 |
| Constraint or not-singular error | 409 |
| Anything else | 500 |
Successful `POST` responses use **201 Created**. Deletes with no body use **204 No Content**.
### Mask error details [#mask-error-details]
Set `MaskErrors` so the `error` field is replaced with the HTTP status text (for example
`"Internal Server Error"`). `type` and `code` are unchanged. Use this in production so internal
details do not leak to clients.
```go title="main.go"
srv, err := rest.NewServer(db, &rest.ServerConfig{
MaskErrors: true,
})
```
### Custom error handler [#custom-error-handler]
`ErrorHandler` replaces the default entirely. If you only want to log errors and still
use the standard JSON shape, call `Server.DefaultErrorHandler` afterwards. Assign it on the same
`*ServerConfig` pointer after `NewServer` returns, so the handler can close over `srv`:
```go title="main.go"
cfg := &rest.ServerConfig{MaskErrors: true}
srv, err := rest.NewServer(db, cfg)
if err != nil {
panic(err)
}
cfg.ErrorHandler = func(w http.ResponseWriter, r *http.Request, op rest.Operation, err error) {
slog.Error("api error", "op", op, "err", err)
srv.DefaultErrorHandler(w, r, op, err)
}
```
If you handle the response yourself, do not also call `DefaultErrorHandler` — it writes the body
and status.
## Request IDs [#request-ids]
Error responses include `request_id` when a request ID is available. `GetReqID` lets you plug in
whatever ID middleware you already use:
```go title="main.go"
srv, err := rest.NewServer(db, &rest.ServerConfig{
GetReqID: func(r *http.Request) string {
return r.Header.Get("X-Request-Id")
},
})
```
If `GetReqID` is nil, the default is:
- **chi**: [`middleware.GetReqID`](https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#GetReqID)
from the request context (populate it with `middleware.RequestID`).
- **stdlib**: the `X-Request-Id` request header.
The generated OpenAPI spec can declare that header via
[`entrest.Config.GlobalRequestHeaders`](https://pkg.go.dev/github.com/lrstanley/entrest#Config)
(`entrest.RequestIDHeader` is a ready-made value). That only documents the header — you still
need `GetReqID` (or the default above) for it to appear on error responses.
## Ent client context [#ent-client-context]
`Server.Handler` always wraps the mux with `UseEntContext`, which stores the `ent.Client` on the
request context via `ent.NewContext`. That is what Ent privacy policies and other context-aware
layers read from.
You do not need to add this middleware yourself when you use the generated handler. Register it
only if some other handler or middleware needs the client on a request that never reaches
`Server.Handler`:
```go title="custom router"
handler := rest.UseEntContext(db)(mux)
```
## JSON responses [#json-responses]
All JSON responses go through `rest.JSON`. Add `pretty=true` as a query parameter (or form value)
to pretty-print the body — handy when exploring the API with curl:
```bash
curl 'http://localhost:8080/pets?pretty=true'
```
`rest.M` is a `map[string]any` alias for one-off JSON objects in custom endpoints.
## Request binding [#request-binding]
`Bind` decodes query parameters, `application/json`, `application/x-www-form-urlencoded`, and
`multipart/form-data` into the generated param structs. Form decoding uses the package-level
`DefaultDecoder` ([go-playground/form](https://github.com/go-playground/form)). Replace or
reconfigure `DefaultDecoder` before serving traffic if you need custom type converters.
Unknown JSON fields are rejected when [`entrest.Config.StrictMutate`](https://pkg.go.dev/github.com/lrstanley/entrest#Config)
is enabled at generation time.
## Related generation-time options [#related-generation-time-options]
These `entrest.Config` flags change how the handler is generated, not how a running server is
configured:
| Option | Effect |
| --- | --- |
| `Handler` | Generate a stdlib `http.Handler`, a chi mounter, or spec-only (`HandlerNone`) |
| `DisableSpecHandler` | Omit `/openapi.json`, `/docs`, `Link` support, and the embedded spec |
| `StrictMutate` | Reject unknown JSON fields on create/update |
| `ListNotFound` | Return **404** (instead of **200**) when a list endpoint matches no rows |
| `WithTesting` | Generate the `enttest` helpers used in [Testing](https://entrest.dev/http-handler/testing) |
See [`entrest.Config`](https://pkg.go.dev/github.com/lrstanley/entrest#Config) for the full set.
# Mount the Handler (https://entrest.dev/http-handler/getting-started)
> **Tutorial**
>
> Mount the generated `rest` package on your HTTP server. Start with
> [Getting Started](https://entrest.dev/guides/getting-started) if you have not set up schemas and codegen yet.
entrest generates an HTTP handler alongside your OpenAPI spec. The handler implements the routes
described in the spec — list, read, create, update, delete, edge sub-resources, filtering, sorting,
pagination, and eager loading — using your `ent.Client`.
## Enable handler generation [#enable-handler-generation]
Handler code is generated when [`entrest.Config.Handler`](https://pkg.go.dev/github.com/lrstanley/entrest#Config)
is not `HandlerNone`. Pick the router integration at generation time:
| Value | Generated `Handler` signature |
| --- | --- |
| `HandlerStdlib` | `Handler() http.Handler` |
| `HandlerChi` | `Handler(r chi.Router)` |
Set it in your `entc.go` file:
```go {14-21,30} title="internal/database/entc.go"
//go:build ignore
package main
import (
"log"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/lrstanley/entrest"
)
func main() {
ex, err := entrest.NewExtension(&entrest.Config{
Handler: entrest.HandlerStdlib,
WithTesting: true,
StrictMutate: true,
})
if err != nil {
log.Fatalf("creating entrest extension: %v", err)
}
err = entc.Generate(
"./database/schema",
&gen.Config{
Target: "./database/ent",
Schema: "github.com/example/my-project/internal/database/schema",
Package: "github.com/example/my-project/internal/database/ent",
},
entc.Extensions(ex),
)
if err != nil {
log.Fatalf("failed to run ent codegen: %v", err)
}
}
```
Run `go generate` as usual. The handler lands in `/rest` next to the embedded `openapi.json`.
Other generation-time options — `WithTesting`, `StrictMutate`, pagination defaults, and so on — are
documented on [`entrest.Config`](https://pkg.go.dev/github.com/lrstanley/entrest#Config) and in
[Runtime Configuration](https://entrest.dev/http-handler/configuration).
## Generated `rest` package [#generated-rest-package]
After codegen, import your project's `/rest` package. The important pieces:
- `rest.NewServer(db, cfg)` — builds a `*rest.Server` from your `ent.Client` and optional
[`ServerConfig`](https://entrest.dev/http-handler/configuration).
- `rest.Server.Handler` — registers routes and returns a handler (stdlib) or mounts onto chi.
- Per-entity methods such as `ListPets`, `GetPet`, `CreatePet` — the same functions the generated
routes call. Use them from custom handlers if you need non-standard endpoints.
- `rest.OpenAPI` — the embedded spec bytes (also served at `GET /openapi.json` by default).
## Mount the handler [#mount-the-handler]
Create the server with your database client, then mount the returned handler on your HTTP server.
### Stdlib (`HandlerStdlib`) [#stdlib-handlerstdlib]
Serve `srv.Handler()` at the process root (or behind a mux that forwards to it). The generated handler
strips `BasePath` and injects the `ent.Client` into the request context for you.
```go {14,40,48} title="main.go"
//nolint:all
package main
import (
"context"
"database/sql"
"fmt"
"net/http"
"entgo.io/ent/dialect/sql/schema"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/example/my-project/internal/database/ent"
"github.com/example/my-project/internal/database/ent/rest"
_ "github.com/example/my-project/internal/database/ent/runtime" // Required by ent.
"modernc.org/sqlite"
)
func main() {
sql.Register("sqlite3", &sqlite.Driver{})
db, err := ent.Open("sqlite3", "file:local.db?cache=shared&_pragma=foreign_keys(1)&_busy_timeout=15")
if err != nil {
panic(err)
}
defer db.Close()
ctx := context.Background()
err = db.Schema.Create(
ctx,
schema.WithDropColumn(true),
schema.WithDropIndex(true),
schema.WithGlobalUniqueID(true),
schema.WithForeignKeys(true),
)
if err != nil {
panic(err)
}
srv, err := rest.NewServer(db, &rest.ServerConfig{})
if err != nil {
panic(err)
}
fmt.Println("running http server")
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Mount("/", srv.Handler())
http.ListenAndServe(":8080", r)
}
```
`http.ListenAndServe(":8080", srv.Handler())` is enough if you do not need extra middleware. Wrapping
with chi (as above) or another `net/http`-compatible router works because `Handler()` returns
`http.Handler`.
### Chi (`HandlerChi`) [#chi-handlerchi]
Register routes on the router (or sub-router) that matches your mount point. Set `BasePath` in
`ServerConfig` to that prefix so spec URLs and `Link` headers stay correct — see
[Configuration — Stdlib vs chi](https://entrest.dev/http-handler/configuration#stdlib-vs-chi).
```go title="main.go"
srv, err := rest.NewServer(db, &rest.ServerConfig{BasePath: "/v1"})
if err != nil {
panic(err)
}
r := chi.NewRouter()
r.Route("/v1", func(r chi.Router) {
srv.Handler(r)
})
http.ListenAndServe(":8080", r)
```
Chi requires v5.0.12 or newer so path parameters populate `http.Request.PathValue`.
## Default routes [#default-routes]
For each schema that is not skipped, entrest registers CRUD routes under kebab-case plural paths
(for example `users`, `pets`). Edge relationships get their own list or read routes:
```text
GET /users
GET /users/{id}
POST /users
PATCH /users/{id}
DELETE /users/{id}
GET /users/{id}/pets # edge sub-resource
GET /pets/{id}/owner # eager-loaded edge read
```
Which operations exist for a schema depends on your annotations and Ent field definitions — a schema
without a create path in the spec will not register `POST`. Skipped schemas (`entrest.WithSkip`) and
disabled operations (`entrest.WithOperation`) are omitted entirely.
When spec serving is enabled (the default), the handler also registers:
- `GET /openapi.json` — embedded OpenAPI document
- `GET /docs` — embedded [Scalar](https://github.com/scalar/scalar) API reference UI
- `GET /` — redirects to `/docs` (stdlib handler only)
Use `?pretty=true` on any JSON response to get indented output.
## Skip route registration [#skip-route-registration]
[`entrest.WithHandler(false)`](https://pkg.go.dev/github.com/lrstanley/entrest#WithHandler) on a schema
or edge keeps the handler methods in `rest` but does not mount routes for that type. The OpenAPI spec
still describes the endpoints unless you disable them separately. Useful when you want the generated
query/update logic but need to wire the route yourself.
```go title="internal/database/schema/pet.go"
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithHandler(false),
}
}
```
Call `srv.ListPets`, `srv.CreatePet`, and so on from your own `http.HandlerFunc` and return JSON with
the generated `rest.JSON` helper, or mount a wrapper that delegates to the generated `Req*` handlers.
## Next steps [#next-steps]
- [Calling Your New API](https://entrest.dev/guides/calling-your-new-api): Open the docs UI and try sample curl requests against a running server.
- [Runtime Configuration](https://entrest.dev/http-handler/configuration): Configure the generated HTTP handler via rest.ServerConfig.
- [API Docs](https://entrest.dev/http-handler/api-docs): Replace the embedded Scalar UI with your own OpenAPI viewer.
- [Authentication & Permissions](https://entrest.dev/http-handler/auth-and-permissions): Wire authentication middleware and Ent privacy policies.
- [Testing](https://entrest.dev/http-handler/testing): Generated helpers for integration-testing the handler.
# Testing (https://entrest.dev/http-handler/testing)
> **How-to guide**
>
> Steps for enabling and using the generated `enttest` request helpers. This page does not cover
> authentication, privacy policies, or testing the `/openapi.json` and `/docs` routes — see
> [Authentication & Permissions](https://entrest.dev/http-handler/auth-and-permissions) and [API Docs](https://entrest.dev/http-handler/api-docs).
Set [`entrest.Config.WithTesting`](https://pkg.go.dev/github.com/lrstanley/entrest#Config) to **true** at
generation time and entrest adds request helpers to your existing `/enttest` package. They wrap
`httptest` around the generated handler so you can exercise real routes, query params, and JSON bodies
against an in-memory database.
`WithTesting` is ignored when [`Handler`](https://pkg.go.dev/github.com/lrstanley/entrest#Config) is
`HandlerNone` — there is no handler to test.
## Enable generation [#enable-generation]
```go title="entc.go"
ex, err := entrest.NewExtension(&entrest.Config{
Handler: entrest.HandlerStdlib,
WithTesting: true,
})
if err != nil {
log.Fatal(err)
}
err = entc.Generate("./schema", &gen.Config{
Target: "./ent",
Package: "example.com/app/ent",
}, entc.Extensions(ex))
```
Run `go generate` as usual. The helpers land in `ent/enttest` alongside Ent's own `enttest.Open` helpers.
## Minimal example [#minimal-example]
After enabling `WithTesting`, a complete test looks like this:
```go title="handler_test.go"
func TestGetPet(t *testing.T) {
ctx := context.Background()
db := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_pragma=foreign_keys(1)")
t.Cleanup(func() { db.Close() })
srv := enttest.NewServer(t, db, nil)
pet := db.Pet.Create().SetName("Riley").SetAge(3).SaveX(ctx)
resp := srv.Request[ent.Pet](ctx, http.MethodGet, "/pets/"+strconv.Itoa(pet.ID), nil).Must(t)
assert.Equal(t, pet.ID, resp.Value.ID)
}
```
Paths are relative to the handler root — with the stdlib handler, `BasePath` stripping is applied for
you, so use `/pets` rather than `/v1/pets` when `BasePath` is `/v1`.
## Test setup [#test-setup]
For shared setup across tests, extract a helper:
```go title="handler_test.go"
func newTestServer(t *testing.T, cfg *rest.ServerConfig) (context.Context, *ent.Client, *enttest.TestServer) {
t.Helper()
ctx := context.Background()
db := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_pragma=foreign_keys(1)")
t.Cleanup(func() { db.Close() })
srv := enttest.NewServer(t, db, cfg)
return ctx, db, srv
}
```
`NewServer` calls `rest.NewServer` and serves the returned handler. Pass `nil` for `ServerConfig` when
defaults are fine — see [Runtime Configuration](https://entrest.dev/http-handler/configuration).
Seed data through the Ent client (`db.Pet.Create()...SaveX(ctx)`), then hit the API with `srv.Request`.
## Making requests [#making-requests]
`Request` is generic over the expected JSON response type:
```go title="handler_test.go"
ctx, db, srv := newTestServer(t, nil)
pet := db.Pet.Create().SetName("Riley").SetAge(3).SaveX(ctx)
// GET a single entity.
resp := srv.Request[ent.Pet](ctx, http.MethodGet, "/pets/"+strconv.Itoa(pet.ID), nil).Must(t)
assert.Equal(t, http.StatusOK, resp.Data.Code)
assert.Equal(t, pet.ID, resp.Value.ID)
// POST with a JSON body (Content-Type is set automatically).
created := srv.Request[ent.Pet](ctx, http.MethodPost, "/pets", map[string]any{
"name": "Orea",
"age": 2,
}).Must(t)
assert.Equal(t, http.StatusCreated, created.Data.Code)
```
Arguments:
| Argument | Purpose |
| --- | --- |
| `ctx` | Request context — forwarded to `httptest.NewRequest` |
| method | HTTP method (`http.MethodGet`, `http.MethodPost`, …) |
| path | Path relative to the handler (no host) |
| body | JSON-marshalable value, `nil`, or `http.NoBody` |
Use `http.NoBody` on GET/DELETE when you want an explicit empty body instead of `nil`.
### List responses [#list-responses]
List endpoints decode into `rest.PagedResponse[T]`:
```go title="handler_test.go"
resp := srv.Request[rest.PagedResponse[ent.Pet]](
ctx, http.MethodGet, "/pets?page=2&per_page=10", nil,
).Must(t)
assert.Equal(t, 2, resp.Value.Page)
assert.Len(t, resp.Value.Content, 10)
```
See [Pagination](https://entrest.dev/openapi-specs/pagination) for the response fields and query parameters.
### Empty bodies [#empty-bodies]
Pass `rest.PagedResponse[ent.Pet]` (or any type) for successful **204 No Content** responses — the body
is not decoded. For deletes where you only care about the status, `Request[string]` also works.
## Success and failure [#success-and-failure]
`Response[T]` holds three fields:
| Field | Contents |
| --- | --- |
| `Data` | `*httptest.ResponseRecorder` — status, headers, raw body |
| `Value` | Unmarshalled JSON on 2xx (except 204) |
| `Error` | `*rest.ErrorResponse` on non-2xx |
Call `.Must(t)` when you expect success — it fails the test if `Error` is set:
```go title="handler_test.go"
resp := srv.Request[ent.Pet](ctx, http.MethodGet, "/pets/99999", nil)
assert.Equal(t, http.StatusNotFound, resp.Data.Code)
require.NotNil(t, resp.Error)
assert.Equal(t, http.StatusNotFound, resp.Error.Code)
```
Skip `.Must(t)` whenever you are asserting on a specific error status. The error shape matches
[Configuration — Error handling](https://entrest.dev/http-handler/configuration#error-handling).
## Custom handlers [#custom-handlers]
If your production server wraps the generated handler in middleware, use `enttest.WithExisting` instead
of `NewServer`:
```go title="handler_test.go"
srv, err := rest.NewServer(db, &rest.ServerConfig{MaxRequestBodyBytes: 100})
require.NoError(t, err)
ts := enttest.WithExisting(t, srv.Handler())
resp := ts.Request[ent.User](ctx, http.MethodPost, "/users", strings.Repeat("x", 101))
require.NotNil(t, resp.Error)
assert.Equal(t, http.StatusRequestEntityTooLarge, resp.Data.Code)
```
### Chi mounts [#chi-mounts]
Build the same router you use in production and pass it to `WithExisting`. Set `BasePath` on
`ServerConfig` to match your mount point, and include the mount prefix in test paths:
```go title="handler_test.go"
cfg := &rest.ServerConfig{BasePath: "/v1"}
srv, err := rest.NewServer(db, cfg)
require.NoError(t, err)
r := chi.NewRouter()
r.Route("/v1", func(r chi.Router) {
srv.Handler(r)
})
ts := enttest.WithExisting(t, r)
resp := ts.Request[rest.PagedResponse[ent.Pet]](ctx, http.MethodGet, "/v1/pets", nil).Must(t)
assert.Equal(t, http.StatusOK, resp.Data.Code)
```
See [Configuration — Stdlib vs chi](https://entrest.dev/http-handler/configuration#stdlib-vs-chi) for mount setup.
## Seeding data [#seeding-data]
`enttest.Multiple` runs a creator function `n` times and returns the results. Useful with `CreateBulk`:
```go title="handler_test.go"
pets := db.Pet.CreateBulk(enttest.Multiple(newPet, db, 25)...).SaveX(ctx)
_ = pets // use in assertions
```
where `newPet` returns `*ent.PetCreate`:
```go title="handler_test.go"
func newPet(db *ent.Client) *ent.PetCreate {
return db.Pet.Create().SetName("test").SetAge(1)
}
```
## Debugging [#debugging]
Chain `WithLogResponses(true)` to print method, path, status, and body for every request:
```go title="handler_test.go"
srv := enttest.NewServer(t, db, nil).WithLogResponses(true)
```
# Annotation Reference (https://entrest.dev/openapi-specs/annotation-reference)
> **Reference**
>
> Per-schema, field, and edge annotations that control what appears in the OpenAPI spec and generated
> HTTP handler. Misplaced annotations fail at codegen — see [Troubleshooting](https://entrest.dev/resources/troubleshooting#annotation-on-the-wrong-graph-level).
> **Usage Location Explanation**
>
> Depending on the annotation used, it can only be applied in specific locations, otherwise code-generation
> **will fail**. The following table explains each usage location:
>
> - schema -- Allows applying to the root of the schema definition (e.g. `func (Pet) Annotations() []ent.Annotation`).
> - edge -- Allows applying directly on an edge (e.g. inside of `func (Pet) Edges() []ent.Edge`).
> - field -- Allows applying directly on a field (e.g. inside of `func (Pet) Fields() []ent.Field`).
### Annotations Summary [#annotations-summary]
| Annotation | Usage Location | Description |
|-------------------------------------------------------|---------------------------|------------------------------------------------------------------------------|
| [WithSkip](#withskip) | schema, edge, field | Sets the schema, edge, or field to be skipped in the REST API. |
| [WithReadOnly](#withreadonly) | field | Sets the field to be read-only in the REST API. |
| [WithExample](#withexample) | field | Sets the OpenAPI example for the specified field. |
| [WithEagerLoad](#witheagerload) | edge | Sets the edge to be eager-loaded in the REST API for each associated entity. |
| [WithSortable](#withsortable) | field | Sets the field to be sortable in the REST API. |
| [WithDefaultSort](#withdefaultsort) | schema | Sets the default sort field for the schema in the REST API. |
| [WithDefaultOrder](#withdefaultorder) | schema | Sets the default sorting order for the schema in the REST API. |
| [WithFilter](#withfilter) | schema, edge, field | Sets the field to be filterable with the provided predicate(s). |
| [WithFilterGroup](#withfiltergroup) | edge, field | Adds the field to a group of other fields that are filtered together. |
| [WithSchema](#withschema) | field | Sets the OpenAPI schema for the specified field. |
| [WithPagination](#withpagination) | schema, edge | Sets the schema to be paginated in the REST API. |
| [WithAllowClientIDs](#withallowclientids) | schema | Sets the schema to allow clients to provide IDs in the CREATE payload. |
| [WithOperationSummary](#withoperationsummary) | schema, edge | Provides an OpenAPI summary for the specified operation. |
| [WithOperationDescription](#withoperationdescription) | schema, edge | Provides an OpenAPI description for the specified operation. |
| [WithAdditionalTags](#withadditionaltags) | schema, edge | Adds additional tags to all operations for this schema/edge. |
| [WithTags](#withtags) | schema, edge | Sets the tags for all operations for this schema/edge. |
| [WithOperationID](#withoperationid) | schema, edge | Provides an OpenAPI operation ID for the specified operation. |
| [WithDescription](#withdescription) | schema, edge, field | Sets the OpenAPI description for the specified schema/edge. |
| [WithMinItemsPerPage](#withminitemsperpage) | schema, edge | Sets an explicit minimum number of items per page for paginated calls. |
| [WithMaxItemsPerPage](#withmaxitemsperpage) | schema, edge | Sets an explicit maximum number of items per page for paginated calls. |
| [WithItemsPerPage](#withitemsperpage) | schema, edge | Sets an explicit default number of items per page for paginated calls. |
| [WithEagerLoadLimit](#witheagerloadlimit) | edge | Sets the limit for the max number of entities to eager-load for the edge. |
| [WithEdgeEndpoint](#withedgeendpoint) | schema, edge | Sets the edge to have an endpoint. |
| [WithEdgeUpdateBulk](#withedgeupdatebulk) | edge | Sets the edge to be bulk updated on the entities associated with the edge. |
| [WithHandler](#withhandler) | schema, edge | Sets the schema/edge to be an HTTP handler generated for it. |
| [WithDeprecated](#withdeprecated) | schema, edge, field | Sets the OpenAPI deprecated flag for the specified schema/edge/field. |
| [WithIncludeOperations](#withincludeoperations) | schema, edge | Includes the specified operations in the REST API for the schema or edge. Schema ops are inherited by edges unless overridden. |
| [WithExcludeOperations](#withexcludeoperations) | schema, edge | Excludes the specified operations in the REST API for the schema or edge. |
### `WithSkip` [#withskip]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithSkip) | usage: schema, edge, field ]
> Sets the schema, edge, or field to be skipped in the REST API. Primarily useful if an entire
> schema shouldn't be queryable, or if there is a sensitive field that should never be returned (but
> sensitive isn't set on the field for some reason).
##### Example [!toc] [#example-toc]
```go title="internal/database/schema/schema_pet.go" {4}
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("some_sensitive_field").Annotations(
entrest.WithSkip(true),
),
}
}
```
### `WithReadOnly` [#withreadonly]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithReadOnly) | usage: field ]
> Sets the field to be read-only in the REST API. **If you want to make a schema or edge read-only,
> use the Operations annotation instead.**
##### Example [!toc] [#example-toc-1]
```go title="internal/database/schema/schema_pet.go" {4}
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("uuid").Annotations(
entrest.WithReadOnly(true),
),
}
}
```
### `WithExample` [#withexample]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithExample) | usage: field ]
> Sets the OpenAPI example for the specified field. This is recommended if it's not obvious what the
> fields purpose is, or what the format could be. Many OpenAPI documentation browsers will use this
> information as an example value within the POST/PATCH body.
##### Example [!toc] [#example-toc-2]
```go title="internal/database/schema/schema_pet.go" {4}
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("uuid").Annotations(
entrest.WithExample("123e4567-e89b-12d3-a456-426655440000"),
),
}
}
```
### `WithEagerLoad` [#witheagerload]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithEagerLoad) | usage: edge ]
> Sets the edge to be eager-loaded in the REST API for each associated entity. Note that edges are not
> eager-loaded by default. Eager-loading, when enabled, means that the configured edge is always fetched
> when the parent entity is fetched (only covering the first level, it does not recurse).
>
> See [Eager Loading](https://entrest.dev/openapi-specs/eager-loading) for more information. See also
> [EntGo: Eager Loading](https://entgo.io/docs/eager-load/).
##### Example [!toc] [#example-toc-3]
```go title="internal/database/schema/schema_pet.go" {4}
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).Annotations(
entrest.WithEagerLoad(true),
),
}
}
```
### `WithSortable` [#withsortable]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithSortable) | usage: field ]
> Sets the field to be sortable in the REST API. Note that only types that can be sorted, will be sortable.
##### Example [!toc] [#example-toc-4]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").Annotations(
entrest.WithSortable(true),
),
}
}
```
### `WithDefaultSort` [#withdefaultsort]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithDefaultSort) | usage: schema ]
> Sets the default sort field for the schema in the REST API. If not specified, will default to the
> `id` field (if it exists on the schema/edge). The provided field must exist on the schema, otherwise
> codegen will fail. You may provide any of the typical fields shown for the `sort` field in the OpenAPI
> specification for this schema. E.g. `id`, `created_at`, `someedge.count` (`.`), etc.
>
> Note that this will also change the way eager-loaded edges which are based on this schema are sorted.
> This is currently the only way to sort eager-loaded data.
##### Example [!toc] [#example-toc-5]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithDefaultSort("name"), // MUST be a valid field!
}
}
```
### `WithDefaultOrder` [#withdefaultorder]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithDefaultOrder) | usage: schema ]
> Sets the default sorting order for the schema in the REST API. If not specified, will default to
> ASC.
>
> Note that this will also change the way eager-loaded edges which are based on this schema are sorted.
> This is currently the only way to sort eager-loaded data.
##### Example [!toc] [#example-toc-6]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithDefaultOrder(entrest.OrderDesc),
}
}
```
### `WithFilter` [#withfilter]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithFilter) | usage: schema, edge, field ]
> Sets the field to be filterable with the provided predicate(s). When applied on an edge with
> `entrest.FilterEdge` applied, it will include the fields associated with the edge that are also filterable.
>
> See [all predicate constants](https://pkg.go.dev/github.com/lrstanley/entrest#Predicate) that
> can be used with `entrest.WithFilter` for more information.
##### Example 1 [!toc] [#example-1-toc]
```go title="internal/database/schema/schema_pet.go" {4}
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").Annotations(
entrest.WithFilter(entrest.FilterGroupArray | entrest.FilterGroupLength) // Bundle using groups.
),
}
}
```
##### Example 2 [!toc] [#example-2-toc]
```go title="internal/database/schema/schema_pet.go" {4}
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").Annotations(
entrest.WithFilter(entrest.FilterEQ | entrest.FilterNEQ), // Or use individual predicates.
),
}
}
```
### `WithFilterGroup` [#withfiltergroup]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithFilterGroup) | usage: edge, field ]
> Adds the field to a group of other fields that are filtered together. Note that only common filter
> options across all of the groups will be supported. The goal of this is to group common fields that
> would be searched together.
>
> You can also use **WithFilterGroup** on edges to also allow any matching groups on the edge to be
> included in this filter group. The group name must match when used on the edge if you want that edge
> to be included in that group.
##### Example [!toc] [#example-toc-7]
Using the following annotations:
```go title="internal/database/schema/schema_user.go" {5,9,13}
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("username").Annotations(
entrest.WithFilter(entrest.FilterGroupEqual|entrest.FilterGroupArray),
entrest.WithFilterGroup("search"),
),
field.String("display_name").Annotations(
entrest.WithFilter(entrest.FilterGroupEqual|entrest.FilterGroupArray),
entrest.WithFilterGroup("search"),
),
field.String("email").Annotations(
entrest.WithFilter(entrest.FilterGroupEqual|entrest.FilterGroupArray),
entrest.WithFilterGroup("search"),
),
field.Enum("type").
NamedValues(
"System", "SYSTEM",
"User", "USER",
).
Default("USER").
Annotations(
entrest.WithExample("USER"),
entrest.WithFilter(entrest.FilterGroupEqualExact|entrest.FilterGroupArray),
),
}
}
```
And the following API query:
```http /search.ihas=foo/
GET /users?type.eq=USER&search.ihas=foo
```
This would effectively result in the following pseudo-code being used behind the scenes:
```
and(type.eq==USER, or(username.ihas==foo, display_name.ihas==foo, email.ihas==foo))
```
### `WithSchema` [#withschema]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithSchema) | usage: field ]
> Sets the OpenAPI schema for a field. This is required for any fields which are JSON based, or don't
> have a pre-defined ent type for the field.
>
> You can use `entrest.SchemaObjectAny` for an object with any properties (effectively `any`).
##### Example [!toc] [#example-toc-8]
```go title="internal/database/schema/schema_pet.go" {4}
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.JSON("uuid", &uuid.UUID{}).Annotations(
entrest.WithSchema(ogen.String()), // Can also use &ogen.Schema{} for full customization.
),
}
}
```
### `WithPagination` [#withpagination]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithPagination) | usage: schema, edge ]
> Sets the schema to be paginated in the REST API. This is not required to be provided unless
> pagination was disabled globally.
>
> See [Pagination](https://entrest.dev/openapi-specs/pagination) for more information.
##### Example [!toc] [#example-toc-9]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithPagination(true),
}
}
```
### `WithAllowClientIDs` [#withallowclientids]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithAllowClientIDs) | usage: schema ]
> Sets the schema to allow clients to provide IDs in the CREATE payload. This is beneficial to allow
> the client to supply UUIDs as primary keys (for idempotency), or when your ID field is a username,
> for example. This is not required if `Config.AllowClientIDs` is enabled.
>
> **SECURITY NOTE**: allowing requests to include the ID field is not recommended, unless you add
> necessary validation (permissions) or disallow resources from being deleted. Otherwise, you may
> allow an attacker to spoof a previously deleted resource, leading to takeover attack vectors.
##### Example [!toc] [#example-toc-10]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithAllowClientIDs(true),
}
}
```
### `WithOperationSummary` [#withoperationsummary]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithOperationSummary) | usage: schema, edge ]
> Provides an [OpenAPI summary](https://swagger.io/specification/#operation-object) for the specified
> operation. This should be a short summary of what the operation does.
##### Example [!toc] [#example-toc-11]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithOperationSummary(entrest.OperationCreate, "Create a pet."),
}
}
```
### `WithOperationDescription` [#withoperationdescription]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithOperationDescription) | usage: schema, edge ]
> Provides an [OpenAPI description](https://swagger.io/specification/#operation-object) for the specified
> operation. This should be a verbose explanation of the operation behavior. CommonMark syntax MAY be used
> for rich text representation.
##### Example [!toc] [#example-toc-12]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithOperationDescription(entrest.OperationCreate, "Create a pet."),
}
}
```
### `WithAdditionalTags` [#withadditionaltags]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithAdditionalTags) | usage: schema, edge ]
> Adds additional [OpenAPI tags](https://swagger.io/specification/#operation-object) to all operations
> for this schema/edge. Tags can be used for logical grouping of operations by resources or any other
> qualifier.
##### Example [!toc] [#example-toc-13]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithAdditionalTags("Foo", "Bar"),
}
}
```
### `WithTags` [#withtags]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithTags) | usage: schema, edge ]
> Sets the [OpenAPI tags](https://swagger.io/specification/#operation-object) for all operations for this
> schema/edge. This will otherwise default to the schema/edge's name(s). Tags can be used for logical grouping
> of operations by resources or any other qualifier.
##### Example [!toc] [#example-toc-14]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithTags("Foo", "Bar"),
}
}
```
### `WithOperationID` [#withoperationid]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithOperationID) | usage: schema, edge ]
> Provides an [OpenAPI operation ID](https://swagger.io/docs/specification/paths-and-operations/#operationid)
> for the specified operation. This should be snake-cased and **must be unique** for the operation.
##### Example [!toc] [#example-toc-15]
```go title="internal/database/schema/schema_pet.go" {3-4}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithOperationID(entrest.OperationCreate, "createPet"),
entrest.WithOperationID(entrest.OperationUpdate, "updatePet"),
}
}
```
### `WithDescription` [#withdescription]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithDescription) | usage: schema, edge, field ]
> Sets the [OpenAPI description](https://swagger.io/specification/#schema-object) for the specified
> schema/edge/field in the REST API. This will otherwise default to the schema/edge/field comment. It's
> recommended to use the field comment rather than setting this annotation when possible.
##### Example [!toc] [#example-toc-16]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithDescription("Pets are the best things ever."),
}
}
```
### `WithMinItemsPerPage` [#withminitemsperpage]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithMinItemsPerPage) | usage: schema, edge ]
> Sets an explicit minimum number of items per page for paginated calls.
>
> See [Pagination](https://entrest.dev/openapi-specs/pagination) for more information.
##### Example [!toc] [#example-toc-17]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithMinItemsPerPage(10),
}
}
```
### `WithMaxItemsPerPage` [#withmaxitemsperpage]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithMaxItemsPerPage) | usage: schema, edge ]
> Sets an explicit maximum number of items per page for paginated calls.
>
> See [Pagination](https://entrest.dev/openapi-specs/pagination) for more information.
##### Example [!toc] [#example-toc-18]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithMaxItemsPerPage(100),
}
}
```
### `WithItemsPerPage` [#withitemsperpage]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithItemsPerPage) | usage: schema, edge ]
> Sets an explicit default number of items per page for paginated calls.
>
> See [Pagination](https://entrest.dev/openapi-specs/pagination) for more information.
##### Example [!toc] [#example-toc-19]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithItemsPerPage(10),
}
}
```
### `WithEagerLoadLimit` [#witheagerloadlimit]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithEagerLoadLimit) | usage: edge ]
> Sets the limit for the max number of entities to eager-load for the edge. There is a global default
> limit for eager-loading, which can be set via the [`EagerLoadLimit`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.EagerLoadLimit)
> configuration option. Defaults to **1000**, and the limit can be disabled by setting the value to `-1`.
>
> See [Eager Loading](https://entrest.dev/openapi-specs/eager-loading) for more information.
##### Example [!toc] [#example-toc-20]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithEagerLoadLimit(100),
}
}
```
### `WithEdgeEndpoint` [#withedgeendpoint]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithEdgeEndpoint) | usage: schema, edge ]
> Sets whether the edge should have a dedicated endpoint. If the edge is eager-loaded and
> [`Config.DisableEagerLoadedEndpoints`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DisableEagerLoadedEndpoints)
> is set, or if [`Config.DisableEdgeEndpoints`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DisableEdgeEndpoints)
> is set, this defaults to false. Not required unless endpoints are disabled globally/per-schema and you want to
> specifically enable one edge, or you want to disable an edge from having an endpoint.
>
> When applied on a schema, it becomes the default for all edges on that schema (individual edges can still override).
>
> See [Eager Loading](https://entrest.dev/openapi-specs/eager-loading) for more information.
##### Example [!toc] [#example-toc-21]
```go title="internal/database/schema/schema_pet.go" {4}
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).Annotations(
entrest.WithEdgeEndpoint(false),
),
}
}
```
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
// Disable edge endpoints for all edges on Pet (override per-edge if needed).
entrest.WithEdgeEndpoint(false),
}
}
```
### `WithEdgeUpdateBulk` [#withedgeupdatebulk]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithEdgeUpdateBulk) | usage: edge ]
> Sets the edge to be bulk updated on the entities associated with the edge. This is disabled by default,
> which will mean that you must use the `add_` and `remove_` object references to associate/disassociate
> entities with the edge.
>
> **This is disabled by default due to the fact that this can lead to accidental disassociation of a massive
> number of entities, if a user doesn't happen to fully understand the implications of providing values to
> the `bulk` field, which would just be `` (sets the non-unique edge to be set to those provided
> values).**
##### Example [!toc] [#example-toc-22]
```go title="internal/database/schema/schema_pet.go" {4}
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).Annotations(
entrest.WithEdgeUpdateBulk(true),
),
}
}
```
### `WithHandler` [#withhandler]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithHandler) | usage: schema, edge ]
> Sets the schema/edge to have an HTTP handler generated for it. Unless a schema/edge is skipped or
> has the specific operation disabled, an HTTP handler/endpoint will be generated for it by default.
> This does not prevent the endpoint from being created within the spec, rather only prevents the
> handler from being mounted. The handler functions will still be generated in case you want to
> build upon them.
>
> See [Mount the Handler](https://entrest.dev/http-handler/getting-started) for more information.
##### Example [!toc] [#example-toc-23]
```go title="internal/database/schema/schema_pet.go" {3}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithHandler(true),
}
}
```
### `WithDeprecated` [#withdeprecated]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithDeprecated) | usage: schema, edge, field ]
> Sets the OpenAPI deprecated flag for the specified schema/edge/field.
##### Example [!toc] [#example-toc-24]
```go title="internal/database/schema/schema_pet.go" {4}
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("some_old_field").Annotations(
entrest.WithDeprecated(true),
),
}
}
```
### `WithIncludeOperations` [#withincludeoperations]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithIncludeOperations) | usage: schema, edge ]
> Includes the specified operations in the REST API. If unset, [`Config.DefaultOperations`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DefaultOperations)
> is used (or, for edges without an explicit setting, the parent schema's operations).
>
> - **Schema**: controls which entity CRUD endpoints are generated. Edges without their own
> include/exclude operations inherit these settings (including dedicated edge endpoints and
> create/update body fields).
> - **Edge**: overrides any inherited schema operations. Controls whether the edge appears in
> create/update request bodies, and whether its dedicated GET endpoint is generated (`read`
> for unique edges, `list` for non-unique).
>
> To keep edge endpoints when the schema has restricted operations, set include/exclude
> operations on the specific edges. To allow updating some edges while disallowing field edits,
> keep `OperationUpdate` on the schema, mark fields with [`WithReadOnly`](#withreadonly), and
> restrict individual edges as needed.
##### Example [!toc] [#example-toc-25]
```go title="internal/database/schema/schema_pet.go" {3-6}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithIncludeOperations(
entrest.OperationCreate,
entrest.OperationDelete,
),
}
}
```
```go title="internal/database/schema/schema_pet.go" {5-8}
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("pets").
Unique().
Annotations(
// Override schema ops so this edge still has a GET endpoint and can be
// updated, even if the schema excluded read/update.
entrest.WithIncludeOperations(
entrest.OperationRead,
entrest.OperationUpdate,
),
),
}
}
```
### `WithExcludeOperations` [#withexcludeoperations]
[ [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/entrest#WithExcludeOperations) | usage: schema, edge ]
> Excludes the specified operations in the REST API. If unset, [`Config.DefaultOperations`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DefaultOperations)
> is used (or, for edges without an explicit setting, the parent schema's operations). See
> [`WithIncludeOperations`](#withincludeoperations) for how schema vs edge operations interact.
##### Example [!toc] [#example-toc-26]
```go title="internal/database/schema/schema_pet.go" {3-6}
func (Pet) Annotations() []ent.Annotation {
return []ent.Annotation{
entrest.WithExcludeOperations(
entrest.OperationCreate,
entrest.OperationDelete,
),
}
}
```
# Codegen Configuration (https://entrest.dev/openapi-specs/configuration)
> **Reference**
>
> Generation-time options on [`entrest.Config`](https://pkg.go.dev/github.com/lrstanley/entrest#Config) that
> shape the OpenAPI spec. Runtime handler options live on
> [Runtime Configuration](https://entrest.dev/http-handler/configuration).
[`entrest.Config`](https://pkg.go.dev/github.com/lrstanley/entrest#Config) is passed to
`entrest.NewExtension` in your `entc.go` file. It runs at code generation time and controls what
paths, parameters, and schemas appear in `openapi.json`.
## Setup [#setup]
```go {14,18-27,30,36-37} title="internal/database/entc.go"
//go:build ignore
// Copyright (c) Liam Stanley . All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package main
import (
"log"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/lrstanley/entrest"
)
func main() {
ex, err := entrest.NewExtension(&entrest.Config{
SpecFromPath: "../base-openapi.json", // Using a base spec to start with, not required.
Handler: entrest.HandlerStdlib,
WithTesting: true,
StrictMutate: true,
ListNotFound: true,
DefaultFilterID: true,
GlobalRequestHeaders: entrest.RequestIDHeader,
GlobalResponseHeaders: entrest.RateLimitHeaders,
})
if err != nil {
log.Fatalf("creating entrest extension: %v", err)
}
err = entc.Generate(
"./database/schema",
&gen.Config{
Target: "./database/ent",
Schema: "github.com/example/my-project/internal/database/schema",
Package: "github.com/example/my-project/internal/database/ent",
Features: []gen.Feature{
gen.FeaturePrivacy,
},
},
entc.Extensions(ex),
)
if err != nil {
log.Fatalf("failed to run ent codegen: %v", err)
}
}
```
`NewExtension(nil)` is equivalent to `NewExtension(&entrest.Config{})`. Most fields have defaults
applied in [`Validate`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.Validate) before
generation runs.
## Default spec metadata [#default-spec-metadata]
If you do not provide [`Spec`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.Spec) or
[`SpecFromPath`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.SpecFromPath), entrest
starts from an empty spec and sets:
| Field | Default |
| --- | --- |
| `openapi` | `3.0.3` |
| `info.title` | `EntGo Rest API` |
| `info.version` | `1.0.0` |
Set API title, description, servers, security schemes, and custom paths through
[Extending the Spec](https://entrest.dev/openapi-specs/extending).
## Operations [#operations]
[`DefaultOperations`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DefaultOperations)
lists which CRUD operations are generated for every schema by default. When unset, all five are
enabled: `create`, `read`, `update`, `delete`, and `list`.
Per-schema or per-edge overrides use
[`WithIncludeOperations`](https://entrest.dev/openapi-specs/annotation-reference#withincludeoperations) and
[`WithExcludeOperations`](https://entrest.dev/openapi-specs/annotation-reference#withexcludeoperations).
Read-only API (list and get only):
```go title="internal/database/entc.go"
ex, err := entrest.NewExtension(&entrest.Config{
DefaultOperations: []entrest.Operation{
entrest.OperationList,
entrest.OperationRead,
},
})
```
An empty `DefaultOperations` list fails validation — at least one operation is required.
## Filtering [#filtering]
[`DefaultFilterID`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DefaultFilterID) adds
`id` filter parameters to list endpoints when your schema does not declare an explicit `id` field
(ent's implicit ID). The filters use the `EQ` and array predicate groups. Edge list filters are not
added unless the edge is marked filterable with
[`WithFilter`](https://entrest.dev/openapi-specs/annotation-reference#withfilter).
## Pagination [#pagination]
List endpoints are paginated by default. Global defaults:
| Option | Default |
| --- | --- |
| [`ItemsPerPage`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.ItemsPerPage) | `10` |
| [`MinItemsPerPage`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.MinItemsPerPage) | `1` |
| [`MaxItemsPerPage`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.MaxItemsPerPage) | `100` |
[`DisablePagination`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DisablePagination)
turns pagination off globally; per-schema overrides use
[`WithPagination`](https://entrest.dev/openapi-specs/annotation-reference#withpagination).
See [Pagination](https://entrest.dev/openapi-specs/pagination) for query parameters, response shape, and
per-schema limits.
## Eager loading [#eager-loading]
| Option | Default | Purpose |
| --- | --- | --- |
| [`DefaultEagerLoad`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DefaultEagerLoad) | `false` | Eager-load every edge |
| [`EagerLoadLimit`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.EagerLoadLimit) | `1000` | Max items per eager-loaded edge (`-1` disables) |
| [`DisableEagerLoadNonPagedOpt`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DisableEagerLoadNonPagedOpt) | `false` | Keep pagination on eager-loaded edge endpoints |
| [`DisableEagerLoadedEndpoints`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DisableEagerLoadedEndpoints) | `false` | Skip edge endpoints when the edge is eager-loaded |
| [`DisableEdgeEndpoints`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DisableEdgeEndpoints) | `false` | Skip all edge endpoints |
Per-edge behavior is controlled with annotations such as
[`WithEagerLoad`](https://entrest.dev/openapi-specs/annotation-reference#witheagerload) and
[`WithEdgeEndpoint`](https://entrest.dev/openapi-specs/annotation-reference#withedgeendpoint).
See [Eager Loading](https://entrest.dev/openapi-specs/eager-loading) for usage and when not to eager-load.
## Tags [#tags]
[`AddEdgesToTags`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.AddEdgesToTags) adds
eager-loaded edge entity names to the `tags` array on parent schema operations. Edge endpoints
already include both parent and child tags. This is mainly useful for large schemas where tags help
clients find related resources in the spec.
## Mutation and list responses [#mutation-and-list-responses]
[`StrictMutate`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.StrictMutate) documents and
enforces **400** responses when create or update bodies contain fields not defined on the schema.
[`ListNotFound`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.ListNotFound) documents **404**
on list endpoints when filters match no rows. The default is **200** with an empty `content` array.
See [Error handling](https://entrest.dev/http-handler/configuration#error-handling) for the response shape.
[`AllowClientIDs`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.AllowClientIDs) allows an
`id` field in create request bodies (for client-supplied UUIDs or natural keys). This is a security
sensitive option — see
[Auth and permissions](https://entrest.dev/http-handler/auth-and-permissions#limit-what-the-api-exposes). Per-schema
control uses
[`WithAllowClientIDs`](https://entrest.dev/openapi-specs/annotation-reference#withallowclientids).
## JSON response bodies [#json-response-bodies]
[`DisablePatchJSONTag`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DisablePatchJSONTag)
skips entrest's JSON tag hook that removes `omitempty` from generated struct tags. With the hook
enabled (default), optional and defaulted fields still appear in JSON responses. Fields marked
`json:"-"` are unchanged.
## Headers and error responses [#headers-and-error-responses]
Global request headers, response headers, and error response schemas are configured on
[`GlobalRequestHeaders`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.GlobalRequestHeaders),
[`GlobalResponseHeaders`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.GlobalResponseHeaders),
and [`GlobalErrorResponses`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.GlobalErrorResponses).
When unset, error responses default to
[`DefaultErrorResponses`](https://pkg.go.dev/github.com/lrstanley/entrest#DefaultErrorResponses)
(**400**, **401**, **403**, **404**, **409**, **429**, **500**).
See [Extending the Spec](https://entrest.dev/openapi-specs/extending) for presets (`RequestIDHeader`,
`RateLimitHeaders`), custom headers, and hooks.
## Handler generation and spec output [#handler-generation-and-spec-output]
| Option | Effect on the spec |
| --- | --- |
| [`Handler`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.Handler) | `HandlerNone` generates only `openapi.json` — no `rest` handler package. `HandlerStdlib` or `HandlerChi` also generate handler code. See [Mount the Handler](https://entrest.dev/http-handler/getting-started). |
| [`DisableSpecHandler`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DisableSpecHandler) | Omits `GET /openapi.json` from the spec, disables embedding the spec in the binary, and turns off `/docs` and `Link` header support in the handler. |
| [`Writer`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.Writer) | Write the spec to a custom `io.Writer` instead of `/rest/openapi.json`. |
| [`Templates`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.Templates) | Additional or replacement `gen.Template` values for handler codegen. |
[`WithTesting`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.WithTesting) generates
`resttest` helpers; it requires a non-`HandlerNone` handler. See
[Testing](https://entrest.dev/http-handler/testing).
When handler generation is enabled and the spec handler is not disabled, the embedded spec is served
at `GET {BasePath}/openapi.json`. See [API Docs](https://entrest.dev/http-handler/api-docs).
## Hooks [#hooks]
[`PreGenerateHook`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.PreGenerateHook) runs
before paths are merged into the base spec. [`PostGenerateHook`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.PostGenerateHook)
runs after merge but before global headers and error responses are applied — use it when custom paths
should receive those globals. [`PreWriteHook`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.PreWriteHook)
runs after the full spec is resolved, immediately before it is written.
Examples are in [Extending the Spec](https://entrest.dev/openapi-specs/extending#pregeneratehook-and-postgeneratehook).
## Per-schema overrides [#per-schema-overrides]
Most global options have matching annotations on schemas, edges, or fields. The full list is on
[Annotation Reference](https://entrest.dev/openapi-specs/annotation-reference).
- [Annotation Reference](https://entrest.dev/openapi-specs/annotation-reference): Every annotation, and where it can be applied.
- [Extending the Spec](https://entrest.dev/openapi-specs/extending): Add custom paths, security schemes, headers, and errors.
- [Pagination](https://entrest.dev/openapi-specs/pagination): Query parameters, response envelope, and when to disable it.
- [Eager Loading](https://entrest.dev/openapi-specs/eager-loading): When to eager-load edges, and the tradeoffs involved.
# Eager Loading (https://entrest.dev/openapi-specs/eager-loading)
> **Explanation**
>
> When to eager-load edges, how entrest exposes them in responses and edge endpoints, and the
> performance and security tradeoffs.
> **Tip**
>
> Before getting started with eager-loading specific functionality in entrest, it is recommended to read
> through the [EntGo Edges](https://entgo.io/docs/schema-edges) documentation, as well as the
> [EntGo Eager Loading](https://entgo.io/docs/eager-load) documentation.
We will use the following example schema in our walk-through, which contains a few different edges:

### What is eager loading? [#what-is-eager-loading]
Eager loading is a feature in EntGo that allows you to eager-load edges on a query, which means that
the edges are fetched along with the parent entity. This is primarily useful when you have a parent
entity, like a `Pet`, and you are confident that you always want to fetch the associated owner of that
pet (the `owner` type, which is a `User`).
In entrest, we build upon this functionality -- to start, all edges by default will have a dedicated
endpoint where you can query an entities associated edges -- these are called **edge endpoints**:
- Unique edge example: `GET /pets/{id}/owner`
- Non-unique edge example: `GET /users/{id}/pets`
### Usage [#usage]
If you enable eager-loading for an edge, say the `owner` edge on the `Pet` schema using the
[`WithEagerLoad`](https://entrest.dev/openapi-specs/annotation-reference#witheagerload) annotation,
calling `GET /pets/{id}` would return something like this:
```json
{
"id": 1,
"name": "Riley",
"age": 2,
"type": "DOG",
"edges": {
"owner": {
"id": 4294967297,
"username": "lrstanley",
"display_name": "Liam Stanley",
"email": "lrstanley@example.com",
"edges": {}
}
}
}
```
And the associated code required:
```go title="internal/database/schema/schema_pet.go" {4}
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).Annotations(
entrest.WithEagerLoad(true),
),
}
}
```
### Additional configuration [#additional-configuration]
- The default maximum number of results that can be eager-loaded for a given edge can be controlled
with the config option [`EagerLoadLimit`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.EagerLoadLimit),
and associated [`WithEagerLoadLimit`](https://entrest.dev/openapi-specs/annotation-reference#witheagerloadlimit)
annotation.
- If you eager load an edge, by default the associated edge endpoint will have its pagination turned off.
This can be turned off with the config option [`DisableEagerLoadNonPagedOpt`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DisableEagerLoadNonPagedOpt).
- If you only ever want to eager load a specific edge and not have a dedicated endpoint, you can use
the [`WithEdgeEndpoint`](https://entrest.dev/openapi-specs/annotation-reference#withedgeendpoint) annotation
to disable the edge endpoint. This can also be configured globally with the config option
[`DisableEagerLoadedEndpoints`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DisableEagerLoadedEndpoints)
(eager-loaded edges only), or [`DisableEdgeEndpoints`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DisableEdgeEndpoints)
(all edges). `WithEdgeEndpoint` can also be set on a schema to disable endpoints for all of its edges.
- All edges can be eager loaded by default (though highly discouraged). See the config option
[`DefaultEagerLoad`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.DefaultEagerLoad).
### When should you not eager load? [#when-should-you-not-eager-load]
Eager loading is a very powerful feature, but it can also be very expensive. For example, if you have
a `User` schema, and an edge **which may have** 1000+ associated entities, eager loading will never
be paginated, so it must always fetch all of the associated entities. Eager loading should only be used
when you are confident that the associated entities will not scale to a large number of entities in almost
all cases.
It's also worth noting that this could be used as an attack vector (i.e. denial-of-service and/or resource-exhaustion).
If a user is in control of an association, and intentionally adds additional entities to the association,
they could overwhelm consumers of your API (response times, memory usage, total response sizes, etc).
As such, **entrest sets a default limit for the number of eager loaded results**. This can be controlled
via the config option [`EagerLoadLimit`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.EagerLoadLimit),
and associated [`WithEagerLoadLimit`](https://entrest.dev/openapi-specs/annotation-reference#witheagerloadlimit)
annotation.
### Examples of good and bad eager loading [#examples-of-good-and-bad-eager-loading]
- Eager loading the `owner` edge on the `Pet` schema: ✔️
- As long as the `owner` edge does not grow to have an extremely large amount of fields, this is
relatively safe.
- Eager loading the `friends` edge on the `Pet` schema: ❌
- As this field is likely user-controlled, this _could_ result in an unknown amount of entities being
fetched, and it is likely a field that is hard to add a limit to.
- Eager loading a `notifications` edge on the `User` schema: ✔️
- Even though `notifications` to some extent are likely user-controlled, it's something that you could
add a limit to (e.g. a given user can only have 200 historical notifications).
# Extending the Spec (https://entrest.dev/openapi-specs/extending)
> **How-to guide**
>
> Add custom paths, security schemes, headers, and error responses to the generated OpenAPI spec via
> a base spec file, `entrest.Config` fields, or generation hooks.
You may want to extend the resulting OpenAPI spec, to add additional endpoints, tweak the schema,
add security schemes, changelog/information, etc. entrest allows a few different ways of extending the OpenAPI
spec.
### `SpecFromPath` [#specfrompath]
Configuration option [`SpecFromPath`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.SpecFromPath) allows
you to provide a path to a JSON file containing the base OpenAPI spec. This is useful if you want to start with a
base spec, and then add additional endpoints/security schemes/etc. This is the simplest option to managing extensions
that is much less tedious than dealing with the `Spec` option.
```go title="internal/database/entc.go" {5}
func main() {
ex, err := entrest.NewExtension(&entrest.Config{
// The path here is dependent on where your file that has "go:generate" is. Take a look at the "kitchensink"
// example for where the base-openapi.json file is located.
SpecFromPath: "../base-openapi.json",
})
// [...]
}
```
Base OpenAPI spec from our kitchensink example, where we register a `/version` endpoint and associated schema:
```json title="_examples/kitchensink/base-openapi.json"
{
"info": {
"title": "Kitchen Sink EntGo Rest API",
"version": "1.0.0"
},
"paths": {
"/version": {
"get": {
"tags": [
"Meta"
],
"summary": "Get service version",
"description": "Get the version of the service.",
"operationId": "getServiceVersion",
"responses": {
"200": {
"description": "Service version information was found.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"description": "Name of cli tool.",
"type": "string"
},
"build_version": {
"description": "Build version.",
"type": "string"
},
"build_commit": {
"description": "VCS commit SHA.",
"type": "string"
},
"build_date": {
"description": "VCS commit date.",
"type": "string"
},
"command": {
"description": "Executable name where the command was called from.",
"type": "string"
},
"go_version": {
"description": "Version of Go that produced this binary.",
"type": "string"
},
"os": {
"description": "Operating system for this build.",
"type": "string"
},
"arch": {
"description": "CPU Architecture for this build.",
"type": "string"
}
},
"required": [
"name",
"go_version",
"os",
"arch"
]
}
}
}
}
}
}
}
}
}
```
After generation, the merged spec is written to `internal/database/ent/rest/openapi.json` and includes
the `/version` endpoint from the base file alongside the generated CRUD paths.
### Request Headers [#request-headers]
[`GlobalRequestHeaders`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.GlobalRequestHeaders) adds
header parameters to every path in the spec. Use this for optional or required headers such as request IDs
or API version strings. Do not use it for authentication — use security schemes instead (see
[Auth and permissions](https://entrest.dev/http-handler/auth-and-permissions#document-security-in-openapi)).
The kitchensink example wires up the built-in preset:
```go title="internal/database/entc.go" {6}
func main() {
ex, err := entrest.NewExtension(&entrest.Config{
SpecFromPath: "../base-openapi.json",
GlobalRequestHeaders: entrest.RequestIDHeader,
})
// [...]
}
```
[`RequestIDHeader`](https://pkg.go.dev/github.com/lrstanley/entrest#RequestIDHeader) documents
`X-Request-Id`. If you use the generated HTTP handlers, configure the runtime side with
[`GetReqID`](https://entrest.dev/http-handler/configuration#request-ids) so the value appears on error responses.
Define your own headers with [`RequestHeaders`](https://pkg.go.dev/github.com/lrstanley/entrest#RequestHeaders):
```go title="internal/database/entc.go"
GlobalRequestHeaders: entrest.RequestHeaders{
"X-Api-Version": {
Description: "API version to use.",
Required: true,
Schema: &ogen.Schema{Type: "string"},
},
},
```
Merge presets with [`RequestHeaders.Append`](https://pkg.go.dev/github.com/lrstanley/entrest#RequestHeaders.Append):
```go title="internal/database/entc.go"
GlobalRequestHeaders: entrest.RequestIDHeader.Append(entrest.RequestHeaders{
"X-Correlation-Id": {
Description: "Correlation ID for distributed tracing.",
Schema: &ogen.Schema{Type: "string"},
},
}),
```
### Response Headers [#response-headers]
[`GlobalResponseHeaders`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.GlobalResponseHeaders) adds
headers to every response on every operation, including shared error responses in `components.responses`.
```go title="internal/database/entc.go" {6}
func main() {
ex, err := entrest.NewExtension(&entrest.Config{
SpecFromPath: "../base-openapi.json",
GlobalResponseHeaders: entrest.RateLimitHeaders,
})
// [...]
}
```
[`RateLimitHeaders`](https://pkg.go.dev/github.com/lrstanley/entrest#RateLimitHeaders) documents
`X-Ratelimit-Limit`, `X-Ratelimit-Remaining`, and `X-Ratelimit-Reset`. The spec only declares them —
your server must set the values.
Define custom response headers the same way, or merge presets with
[`ResponseHeaders.Append`](https://pkg.go.dev/github.com/lrstanley/entrest#ResponseHeaders.Append):
```go title="internal/database/entc.go"
GlobalResponseHeaders: entrest.RateLimitHeaders.Append(entrest.ResponseHeaders{
"X-Request-Id": {
Description: "Echo of the request ID.",
Schema: &ogen.Schema{Type: "string"},
},
}),
```
### Error Responses [#error-responses]
[`GlobalErrorResponses`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.GlobalErrorResponses) maps
HTTP status codes to JSON error schemas. When unset, entrest uses
[`DefaultErrorResponses`](https://pkg.go.dev/github.com/lrstanley/entrest#DefaultErrorResponses): **400**,
**401**, **403**, **404**, **409**, **429**, and **500**. The shape matches what the generated handlers
return (`error`, `type`, `code`, `request_id`, `timestamp`). See
[Error handling](https://entrest.dev/http-handler/configuration#error-handling).
Some operations omit certain codes:
- **404** is not listed on list endpoints unless
[`ListNotFound`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.ListNotFound) is enabled.
- **409** is only listed on create and update operations.
Replace the defaults entirely:
```go title="internal/database/entc.go"
GlobalErrorResponses: entrest.ErrorResponses{
http.StatusInternalServerError: {
Type: "object",
Properties: ogen.Properties{
ogen.Property{Name: "message", Schema: &ogen.Schema{Type: "string"}},
},
Required: []string{"message"},
},
},
```
Or extend them with [`ErrorResponses.Append`](https://pkg.go.dev/github.com/lrstanley/entrest#ErrorResponses.Append)
and [`ErrorResponseObject`](https://pkg.go.dev/github.com/lrstanley/entrest#ErrorResponseObject):
```go title="internal/database/entc.go"
GlobalErrorResponses: entrest.DefaultErrorResponses.Append(entrest.ErrorResponses{
http.StatusTeapot: entrest.ErrorResponseObject(http.StatusTeapot),
}),
```
Paths added in [`PostGenerateHook`](#pregeneratehook-and-postgeneratehook) receive the global error
responses and headers as well.
### `Spec` Configuration Option [#spec-configuration-option]
Configuration option [`Spec`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.Spec) allows you to provide
a custom OpenAPI spec through the `ogen.Spec` type provided by [ogen](https://github.com/ogen-go/ogen). This is
more of an advanced option, as it allows you to use additional Go logic to add additional generated endpoints or
similar. This spec is effectively treated like the "base" in which the generated spec is merged into. A basic
example:
```go title="internal/database/entc.go" {3-13}
func main() {
ex, err := entrest.NewExtension(&entrest.Config{
Spec: &ogen.Spec{
Info: ogen.Info{
Title: "My Sample API",
Description: "This is a sample API.",
},
Components: &ogen.Components{
Schemas: map[string]*ogen.Schema{
"FooBar": {Type: "string"},
},
},
},
})
// [...]
}
```
### `PreGenerateHook` and `PostGenerateHook` [#pregeneratehook-and-postgeneratehook]
Configuration option [`PreGenerateHook`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.PreGenerateHook)
and [`PostGenerateHook`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.PostGenerateHook) are
similar to the `Spec` option, but you get access to the ent graph. Primarily useful if you need to
extend the spec with ent-specific information, or you want to reuse content from the generated configuration,
and build upon it.
Example with a `PreGenerateHook`:
```go title="internal/database/entc.go" {3-7}
func main() {
ex, err := entrest.NewExtension(&entrest.Config{
PreGenerateHook: func(g *gen.Graph, spec *ogen.Spec) error {
// Example:
spec.Components.Schemas["FooBar"] = &ogen.Schema{Type: "string"}
return nil
},
})
// [...]
}
```
Same thing with a `PostGenerateHook`, but it runs after the spec has been generated:
```go title="internal/database/entc.go" {3-7}
func main() {
ex, err := entrest.NewExtension(&entrest.Config{
PostGenerateHook: func(g *gen.Graph, spec *ogen.Spec) error {
// Example:
spec.Components.Schemas["FooBar"] = &ogen.Schema{Type: "string"}
return nil
},
})
// [...]
}
```
# Pagination (https://entrest.dev/openapi-specs/pagination)
> **Explanation**
>
> How list pagination works in entrest — query parameters, response envelope, and when to disable it.
> For codegen defaults, see [Codegen Configuration — Pagination](https://entrest.dev/openapi-specs/configuration#pagination).
## What is Pagination? [#what-is-pagination]
entrest supports pagination for all **list** operations by default. Pagination at a high-level ensures
that as the data returned by your API grows, there is a linear request size and query time/complexity,
and you can better enforce rate limits for querying your API. If you cannot be confident that the number
of results will not grow beyond a reasonable size, you should keep pagination enabled for any schemas
where that may apply.
## Configuration [#configuration]
Pagination has a few configuration options in a few locations:
- Turning pagination on/off.
- **Globally**: with the `DisablePagination` [config](https://pkg.go.dev/github.com/lrstanley/entrest#Config)
option.
- **Per-schema**: with the [`WithPagination`](https://entrest.dev/openapi-specs/annotation-reference#withpagination)
annotation.
- Adjusting the default, minimum and maximum number of results per page.
- **Globally**: with the `ItemsPerPage`, `MinItemsPerPage`, and `MaxItemsPerPage`
[config](https://pkg.go.dev/github.com/lrstanley/entrest#Config) options.
- **Per-schema**: with the [`WithItemsPerPage`](https://entrest.dev/openapi-specs/annotation-reference#withitemsperpage),
[`WithMinItemsPerPage`](https://entrest.dev/openapi-specs/annotation-reference#withminitemsperpage), and
[`WithMaxItemsPerPage`](https://entrest.dev/openapi-specs/annotation-reference#withmaxitemsperpage) annotations.
## Example of querying a paginated endpoint [#example-of-querying-a-paginated-endpoint]
Using our [example API](https://entrest.dev/guides/getting-started), and some of the [example queries](https://entrest.dev/guides/calling-your-new-api),
we have the `/users/{id}/pets` endpoint which returns a list of pets associated with the user. Since the
user could have many pets (who knows, maybe they run a cat cafe), this is a perfect use case for pagination.
```bash
curl --request GET \
--url 'http://localhost:8080/users/4294967297/pets?pretty=true&page=1&per_page=5'
```
```json
{
"page": 1,
"total_count": 124,
"last_page": 25,
"is_last_page": false,
"content": [
{
"id": 1,
"name": "Riley",
"age": 1,
"type": "DOG",
"edges": {
"owner": {
"id": 4294967297,
"username": "lrstanley",
"display_name": "Liam Stanley",
"email": "lrstanley@example.com",
"edges": {}
}
}
},
{
"id": 2,
"name": "Orea",
"age": 2,
"type": "DOG",
"edges": {
"owner": {
"id": 4294967297,
"username": "lrstanley",
"display_name": "Liam Stanley",
"email": "lrstanley@example.com",
"edges": {}
}
}
},
{
"id": 3,
"name": "Prince",
"age": 9,
"type": "CAT",
"edges": {
"owner": {
"id": 4294967297,
"username": "lrstanley",
"display_name": "Liam Stanley",
"email": "lrstanley@example.com",
"edges": {}
}
}
},
{
"id": 4,
"name": "Rex",
"age": 2,
"type": "DOG",
"edges": {
"owner": {
"id": 4294967297,
"username": "lrstanley",
"display_name": "Liam Stanley",
"email": "lrstanley@example.com",
"edges": {}
}
}
},
{
"id": 5,
"name": "Lucky",
"age": 4,
"type": "DOG",
"edges": {
"owner": {
"id": 4294967297,
"username": "lrstanley",
"display_name": "Liam Stanley",
"email": "lrstanley@example.com",
"edges": {}
}
}
}
]
}
```
You will notice that we provided the `page` parameter (not required for the first page), and the
`per_page` parameter (to control how many results we want per page). `per_page` will be limited to
the `MinItemsPerPage` and `MaxItemsPerPage` values, and they have relatively sane defaults.
As we only requested `5` results, and there are a total of `124` results as shown in the `total_count`
field, we can see that there are `25` pages in total. You can use the `last_page` or `is_last_page`
fields to determine if we are on the last page, or if there are more pages to fetch.
### Python example [#python-example]
Below is an example of how you might achieve collecting all results from a paginated endpoint in Python:
```python
page = 1
results = []
while True:
print(f"fetching page {page}...")
resp = requests.get(f"http://localhost:8080/users/4294967297/pets?page={page}&per_page=100")
data = resp.json()
results += data["content"]
if data["is_last_page"]:
break
page += 1
print(results)
```
### Go example [#go-example]
Below is an example of how you might achieve collecting all results from a paginated endpoint in Go:
```go
package main
"encoding/json/v2"
"fmt"
"net/http"
)
type Pet struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Type string `json:"type"`
// [...]
}
// Recommend a generic type wrapper, since you'd likely have multiple endpoints which re-use the same
// base paginated structure.
type Paged[T any] struct {
Page int `json:"page"`
TotalCount int `json:"total_count"`
LastPage int `json:"last_page"`
IsLastPage bool `json:"is_last_page"`
Content []*T `json:"content"`
}
func main() {
page := 1
var pets []*Pet
var pageResults Paged[Pet]
for {
resp, err := http.Get(fmt.Sprintf("http://localhost:8080/users/4294967297/pets?page=%d&per_page=100", page))
if err != nil {
panic(err)
}
err = json.UnmarshalRead(resp.Body, &pageResults)
if err != nil {
panic(err)
}
resp.Body.Close()
pets = append(pets, pageResults.Content...)
if pageResults.IsLastPage {
break
}
page += 1
}
fmt.Printf("total pets: %d\n", len(pets))
}
```
# Architecture (https://entrest.dev/resources/architecture)
> **Explanation**
>
> How entrest generates OpenAPI specs and HTTP handlers at `go generate` time. For runtime handler
> behavior, see [Runtime Configuration](https://entrest.dev/http-handler/configuration). For codegen options,
> see [Codegen Configuration](https://entrest.dev/openapi-specs/configuration).
entrest is an [entc extension](https://entgo.io/docs/code-gen/#extensions). 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
`/rest/`).

## Codegen pipeline [#codegen-pipeline]
At a high level, generation happens in three phases: prepare the graph, build and merge the OpenAPI
spec, then emit handler templates.
```mermaid
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]
```
### 1. Graph preparation [#1-graph-preparation]
Ent loads your schema files into a `*gen.Graph`. entrest registers two entc hooks:
1. **JSON tag patch** (unless [`DisablePatchJSONTag`](https://entrest.dev/openapi-specs/configuration#json-response-bodies)
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`](https://pkg.go.dev/github.com/lrstanley/entrest#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.
### 2. OpenAPI spec generation [#2-openapi-spec-generation]
For each schema node in the graph:
1. **Skip** schemas (or edges) marked with [`WithSkip`](https://entrest.dev/openapi-specs/annotation-reference#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`](https://entrest.dev/openapi-specs/annotation-reference#withedgeendpoint)
allows it.
4. Unless [`DisableSpecHandler`](https://entrest.dev/openapi-specs/configuration#handler-generation-and-spec-output)
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.
#### Base spec sources [#base-spec-sources]
The merge target is chosen in this order:
| Source | Config field |
| --- | --- |
| In-memory spec | [`Config.Spec`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.Spec) |
| JSON file on disk | [`Config.SpecFromPath`](https://pkg.go.dev/github.com/lrstanley/entrest#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](https://entrest.dev/openapi-specs/extending)
for adding custom paths, security schemes, and metadata.
#### Global enrichment (after merge) [#global-enrichment-after-merge]
These run **after** [`PostGenerateHook`](https://entrest.dev/openapi-specs/extending#pregeneratehook-and-postgeneratehook)
and before the spec is written:
- [`GlobalErrorResponses`](https://entrest.dev/openapi-specs/configuration#headers-and-error-responses)
- [`GlobalRequestHeaders`](https://entrest.dev/openapi-specs/configuration#headers-and-error-responses)
- [`GlobalResponseHeaders`](https://entrest.dev/openapi-specs/configuration#headers-and-error-responses)
Custom paths added in `PreGenerateHook` receive globals automatically. Paths added in
`PostGenerateHook` also receive them because globals are applied after that hook.
[`PreWriteHook`](https://entrest.dev/openapi-specs/extending#pregeneratehook-and-postgeneratehook) runs last —
after globals — and receives only the finished `*ogen.Spec` (no graph access).
The final document is written to `/rest/openapi.json` unless you set a custom
[`Writer`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.Writer).
### 3. Handler generation [#3-handler-generation]
When [`Config.Handler`](https://pkg.go.dev/github.com/lrstanley/entrest#Config.Handler) is not
`HandlerNone`, entc renders Go templates into `/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`](https://entrest.dev/http-handler/testing) adds request helpers under `/enttest/`.
`HandlerNone` generates only the spec — no handler package.
## Config vs annotations [#config-vs-annotations]
entrest uses two configuration layers that resolve at codegen time:
```mermaid
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]
```
### Global: `entrest.Config` [#global-entrestconfig]
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](https://entrest.dev/openapi-specs/configuration) for the full field list.
### Per-entity: `entrest` annotations [#per-entity-entrest-annotations]
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`](https://entrest.dev/openapi-specs/annotation-reference#withexcludeoperations)
on an edge overrides the parent schema's operation set.
See [Annotation Reference](https://entrest.dev/openapi-specs/annotation-reference) for every annotation and
its allowed attachment point.
### Runtime: `rest.ServerConfig` [#runtime-restserverconfig]
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](https://entrest.dev/http-handler/configuration).
## Generated artifacts [#generated-artifacts]
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`.
## Path naming [#path-naming]
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}`).
## Related pages [#related-pages]
- [Codegen Configuration](https://entrest.dev/openapi-specs/configuration) — `entrest.Config` fields
- [Extending the Spec](https://entrest.dev/openapi-specs/extending) — base spec, hooks, security schemes
- [Annotation Reference](https://entrest.dev/openapi-specs/annotation-reference) — per-schema overrides
- [Mount the Handler](https://entrest.dev/http-handler/getting-started) — wiring the generated `rest` package
- [Troubleshooting](https://entrest.dev/resources/troubleshooting) — common codegen and runtime errors
# Best Practices (https://entrest.dev/resources/best-practices)
> **Explanation**
>
> OpenAPI conventions entrest follows and how to keep generated specs useful for validators and client
> generators.
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](https://github.com/IBM/openapi-validator)) and with tools that generate HTTP clients from a spec.
## Validation [#validation]
Generated specs are checked with [libopenapi-validator](https://github.com/pb33f/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](https://entrest.dev/resources/recommended-tools).
If validation fails after customizing the spec, the failure is usually a duplicate `operationId`, a conflicting path merge, or an invalid `$ref`.
## Reuse components instead of duplicating schemas [#reuse-components-instead-of-duplicating-schemas]
entrest puts shared pieces under `components` and references them with `$ref`:
| Component | Purpose |
| --- | --- |
| `PagedResponse` | Shared pagination metadata (`page`, `last_page`, `is_last_page`, `total_count`) |
| `Page`, `PrettyResponse`, `FilterOperation` | Shared query parameters |
| `ErrorBadRequest`, `ErrorNotFound`, … | Shared error responses |
| `{Entity}TypeEnum`, etc. | Hoisted enum schemas |
| `{Entity}Create`, `{Entity}Update`, `{Entity}Read` | Per-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](https://entrest.dev/openapi-specs/extending), prefer adding reusable components and `$ref`ing them rather than copying JSON into each path.
## Operation layout [#operation-layout]
Each generated operation includes:
- **operationId** — Unique across the whole spec. Default pattern: `createUser`, `getUser`, `updateUser`, `deleteUser`, `listUsers`, `listUserPets`, etc. Override with [`WithOperationID`](https://entrest.dev/openapi-specs/annotation-reference#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`](https://entrest.dev/openapi-specs/configuration#tags) optionally adds eager-loaded entity names to parent operations.
- **summary** and **description** — Defaults are generated from the entity name; override with [`WithOperationSummary`](https://entrest.dev/openapi-specs/annotation-reference#withoperationsummary) and [`WithOperationDescription`](https://entrest.dev/openapi-specs/annotation-reference#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`).
## Request and response schemas [#request-and-response-schemas]
### Separate schemas per operation [#separate-schemas-per-operation]
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).
### Field metadata [#field-metadata]
Where ent (or your annotations) provide the data, the spec includes:
| Source | OpenAPI field |
| --- | --- |
| `field.Comment()` or [`WithDescription`](https://entrest.dev/openapi-specs/annotation-reference#withdescription) | `description` |
| [`WithExample`](https://entrest.dev/openapi-specs/annotation-reference#withexample) | `example` |
| Ent field `Default()` | `default` |
| Go integer width (`int8`, `uint32`, …) | `minimum` / `maximum` on integers |
| `Optional()` + `Nillable()` | `nullable: true` |
| Ent enum values | `enum` (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`](https://entrest.dev/openapi-specs/annotation-reference#withskip) or [`WithReadOnly`](https://entrest.dev/openapi-specs/annotation-reference#withreadonly) to keep fields out of write schemas too.
JSON fields without a native OpenAPI type need [`WithSchema`](https://entrest.dev/openapi-specs/annotation-reference#withschema) (for example `SchemaObjectAny` for arbitrary objects).
### Pagination and list parameters [#pagination-and-list-parameters]
List endpoints document `page`, `per_page`, `sort`, and `order` with defaults and bounds from [Configuration](https://entrest.dev/openapi-specs/configuration#pagination). Paginated list responses use `allOf` with `PagedResponse` and a `content` array — see [Pagination](https://entrest.dev/openapi-specs/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”).
## Error responses [#error-responses]
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:
```json
{
"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:
| Status | Omitted when |
| --- | --- |
| **404** on list | [`ListNotFound`](https://entrest.dev/openapi-specs/configuration#mutation-and-list-responses) is `false` (default). Empty filter results return **200** with an empty `content` array. |
| **409** | Operation is not create or update. |
Customize the set with [`GlobalErrorResponses`](https://entrest.dev/openapi-specs/configuration#headers-and-error-responses) or extend bodies in [Extending the Spec](https://entrest.dev/openapi-specs/extending).
## Optimizing for client generators [#optimizing-for-client-generators]
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`](https://entrest.dev/openapi-specs/configuration#handler-generation-and-spec-output) to `HandlerNone` and feed `openapi.json` directly into your generator.
## Keeping the spec smaller [#keeping-the-spec-smaller]
Large Ent graphs produce large specs. Most of the bulk is filter parameters and eager-loaded edge schemas.
| Approach | Effect |
| --- | --- |
| Eager-load only edges callers need | Fewer nested schemas in read/list responses. See [Eager Loading](https://entrest.dev/openapi-specs/eager-loading). |
| [`DisableEagerLoadedEndpoints`](https://entrest.dev/openapi-specs/configuration#eager-loading) | Drops redundant edge paths when the edge is already on the parent response. |
| [`DisableEdgeEndpoints`](https://entrest.dev/openapi-specs/configuration#eager-loading) or per-edge [`WithEdgeEndpoint`](https://entrest.dev/openapi-specs/annotation-reference#withedgeendpoint) | Fewer paths and operations. |
| Narrow [`WithFilter`](https://entrest.dev/openapi-specs/annotation-reference#withfilter) usage | Fewer query parameters per list endpoint. |
| [`WithExcludeOperations`](https://entrest.dev/openapi-specs/annotation-reference#withexcludeoperations) | Drop unused CRUD operations for internal entities. |
| Leave [`AddEdgesToTags`](https://entrest.dev/openapi-specs/configuration#tags) disabled | Less tag noise on operations (edge endpoints still get both tags). |
| [`DisablePagination`](https://entrest.dev/openapi-specs/configuration#pagination) on small, bounded lists | Simpler 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.
## Example: documenting a field for the spec [#example-documenting-a-field-for-the-spec]
```go title="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`.
## Related pages [#related-pages]
- [Codegen Configuration](https://entrest.dev/openapi-specs/configuration) — generation-time options that shape the spec
- [Annotation Reference](https://entrest.dev/openapi-specs/annotation-reference) — per-field and per-schema overrides
- [Extending the Spec](https://entrest.dev/openapi-specs/extending) — base spec, hooks, security schemes
- [Runtime Configuration](https://entrest.dev/http-handler/configuration#error-handling) — runtime behavior that matches documented errors
# Recommended Tools (https://entrest.dev/resources/recommended-tools)
> **Reference**
>
> Curated tools for validating, diffing, viewing, and generating clients from your `openapi.json`.
## Validation [#validation]
| Tool | Notes |
| --- | --- |
| [IBM OpenAPI Validator](https://github.com/IBM/openapi-validator) | CLI linter with many Spectral-based rules aimed at production APIs. Good second check after entrest generation. |
| [Spectral](https://github.com/stoplightio/spectral) | Flexible rule engine; use the `openapi` ruleset or custom rules for your org. |
| [libopenapi-validator](https://github.com/pb33f/libopenapi-validator) | What entrest uses in its own tests — validates document structure and references. |
| [OpenAPI Doctor](https://pb33f.io/doctor/) | All-in-one OpenAPI toolkit from the libopenapi authors — validation, bundling, overlays, and more. |
Example with IBM OpenAPI Validator (requires Node):
```bash
npx ibm-openapi-validator path/to/openapi.json
```
## Diffing [#diffing]
| Tool | Notes |
| --- | --- |
| [openapi-changes](https://pb33f.io/openapi-changes/) | Breaking-change detection with console, markdown, JSON, and HTML reports. Works across local files, git revisions, and GitHub-hosted specs. |
| [oasdiff](https://github.com/Tufin/oasdiff) | Breaking-change detection between two spec versions. Useful in CI when the spec is generated on every build. |
| [openapi-diff](https://github.com/OpenAPITools/openapi-diff) | Human-readable diff of paths, parameters, and schemas. |
## Viewing and manual testing [#viewing-and-manual-testing]
The generated handler serves the spec at `GET {basePath}/openapi.json` and embeds Scalar at `/docs` by default. See [API Docs](https://entrest.dev/http-handler/api-docs) for replacing or disabling the bundled UI.
[printing press](https://pb33f.io/printing-press/) generates static API docs from `openapi.json` — offline-friendly HTML you can zip, host, or ship in a release bundle. Includes `llms.txt` and markdown output if you want docs aimed at agents as well as humans.
For ad-hoc requests against a running server, any OpenAPI-aware client (Postman, Bruno, Insomnia) can import `openapi.json`.
## Client generation [#client-generation]
Common generators that work well with entrest's component-heavy 3.0.3 specs:
- [oapi-codegen](https://github.com/oapi-codegen/oapi-codegen) (Go)
- [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) (many languages)
- [Speakeasy](https://www.speakeasy.com/) (commercial, multiple languages)
Use stable [`operationId`](https://entrest.dev/resources/best-practices#operation-layout) values and tags when picking generator options. See [Best Practices](https://entrest.dev/resources/best-practices#optimizing-for-client-generators).
# Troubleshooting (https://entrest.dev/resources/troubleshooting)
> **How-to guide**
>
> Problem-oriented fixes for errors you may hit during `go generate` or while running the generated
> HTTP handler. For architecture context, see [Architecture](https://entrest.dev/resources/architecture).
## Code generation failures [#code-generation-failures]
### Annotation on the wrong graph level [#annotation-on-the-wrong-graph-level]
**Symptom**
```
failed to validate annotations: annotation field "EagerLoad" is set on "Pet" schema type,
but only field and edge are allowed
```
**Cause**
An `entrest` annotation is attached to a schema, field, or edge that does not support it. Each
annotation has a fixed usage location — see the summary table in
[Annotation Reference](https://entrest.dev/openapi-specs/annotation-reference).
**Fix**
Move the annotation to the correct `Annotations()` block. For example, [`WithEagerLoad`](https://entrest.dev/openapi-specs/annotation-reference#witheagerload)
belongs on an edge definition inside `Edges()`, not on the schema root.
---
### No operations generated [#no-operations-generated]
**Symptom**
```
spec generated no operations, thus no spec paths can be generated
```
**Cause**
Every schema is skipped (`WithSkip`) or [`DefaultOperations`](https://entrest.dev/openapi-specs/configuration#operations)
is empty.
**Fix**
Ensure at least one schema has CRUD operations enabled. An empty `DefaultOperations` slice fails
validation — you need at least one operation. Check for accidental `WithSkip(true)` on all schemas.
---
### Unsupported field type in the spec [#unsupported-field-type-in-the-spec]
**Symptom**
```
no openapi type exists for type "..." of field metadata
```
**Cause**
Ent field type has no built-in OpenAPI mapping — common for `field.JSON` with custom Go types.
**Fix**
Add [`WithSchema`](https://entrest.dev/openapi-specs/annotation-reference#withschema) on the field:
```go title="internal/database/schema/post.go"
field.JSON("metadata", &Metadata{}).Annotations(
entrest.WithSchema(ogen.String()), // or a full &ogen.Schema{}
)
```
Use `entrest.SchemaObjectAny` for arbitrary JSON objects.
---
### Invalid default sort field [#invalid-default-sort-field]
**Symptom**
Panic during generation:
```
default sort field "name" on schema "Pet" does not exist
```
**Cause**
[`WithDefaultSort`](https://entrest.dev/openapi-specs/annotation-reference#withdefaultsort) references a field
that is not on the schema or is not sortable.
**Fix**
Point `WithDefaultSort` at a real field name and ensure the field has
[`WithSortable(true)`](https://entrest.dev/openapi-specs/annotation-reference#withsortable) if needed.
---
### Base spec path or format errors [#base-spec-path-or-format-errors]
**Symptom**
```
failed to open spec from path "../base-openapi.json": no such file or directory
```
or JSON decode errors.
**Cause**
[`SpecFromPath`](https://entrest.dev/openapi-specs/extending#specfrompath) points to a file that does not
exist relative to the `go:generate` working directory, or the file is not valid OpenAPI JSON.
**Fix**
Use a path relative to the file that contains the `go:generate` directive. The
[kitchensink example](https://github.com/lrstanley/entrest/tree/master/_examples/kitchensink) shows
the expected layout. Validate the JSON with
[Recommended Tools](https://entrest.dev/resources/recommended-tools#validation) before generation.
---
### Spec and SpecFromPath both set [#spec-and-specfrompath-both-set]
**Symptom**
```
Config.Spec and Config.SpecFromPath cannot be provided at the same time
```
**Fix**
Choose one base spec source. Use `SpecFromPath` for a static JSON file; use `Spec` when you need Go
logic to build the base in code.
---
### Duplicate operationId after extending the spec [#duplicate-operationid-after-extending-the-spec]
**Symptom**
OpenAPI validation fails, or client generators report duplicate operation IDs.
**Cause**
A custom path in your base spec or a [`PostGenerateHook`](https://entrest.dev/openapi-specs/extending#pregeneratehook-and-postgeneratehook)
defines an `operationId` that collides with a generated one (for example `listUsers`).
**Fix**
Rename the conflicting `operationId` in your base spec or override the generated ID with
[`WithOperationID`](https://entrest.dev/openapi-specs/annotation-reference#withoperationid). Run
`npx ibm-openapi-validator path/to/openapi.json` to catch duplicates early — see
[Best Practices — Validation](https://entrest.dev/resources/best-practices#validation).
---
### Path merge conflict (strict merge) [#path-merge-conflict-strict-merge]
**Symptom**
```
path "/users" already exists in the spec
```
**Cause**
Your base spec defines a path that entrest also generates. The public `MergeSpec` API uses strict
merging; internally entrest uses overlap merge, but component key conflicts can still surface.
**Fix**
Rename the custom path in your base spec (for example `/users` → `/internal/users`) or remove the
generated operation with [`WithExcludeOperations`](https://entrest.dev/openapi-specs/annotation-reference#withexcludeoperations).
---
## Runtime errors [#runtime-errors]
### 404 on all routes behind a reverse proxy [#404-on-all-routes-behind-a-reverse-proxy]
**Symptom**
Routes work locally but return 404 when mounted behind a prefix such as `/api/v1`.
**Cause**
`BasePath` does not match the mount point, or the path is stripped twice (common with stdlib handler
wrapped in an extra `StripPrefix`).
**Fix**
Set [`BasePath`](https://entrest.dev/http-handler/configuration#base-url-and-path) to the mount prefix
(for example `/api/v1`). With the stdlib handler, do **not** also wrap the returned handler in a
second prefix strip — entrest applies `StripPrefix` internally. See
[Stdlib vs chi](https://entrest.dev/http-handler/configuration#stdlib-vs-chi).
---
### Chi path parameters empty [#chi-path-parameters-empty]
**Symptom**
`GET /pets/{id}` returns 400 or fails to parse the ID.
**Cause**
chi v5.0.12 or newer is required so path parameters populate `http.Request.PathValue`.
**Fix**
Upgrade chi: `go get github.com/go-chi/chi/v5@v5.0.12` or later.
---
### 403 on background jobs or migrations [#403-on-background-jobs-or-migrations]
**Symptom**
Ent queries that worked before privacy policies now return `privacy.Deny` or fail silently.
**Cause**
[Privacy policies](https://entrest.dev/http-handler/auth-and-permissions#authorization-with-privacy-policies)
run on every Ent call. Internal code paths (workers, seed scripts, migrations) do not pass through
auth middleware, so the context lacks the identity your policies expect.
**Fix**
Use Ent's [decision context override](https://entgo.io/docs/privacy/#decision-context) for trusted
internal work:
```go
ctx = privacy.DecisionContext(ctx, privacy.Allow)
```
See [Background and internal calls](https://entrest.dev/http-handler/auth-and-permissions#background-and-internal-calls).
---
### 400 on create/update with unknown JSON fields [#400-on-createupdate-with-unknown-json-fields]
**Symptom**
```
unknown field "extra_field"
```
**Cause**
[`StrictMutate`](https://entrest.dev/openapi-specs/configuration#mutation-and-list-responses) is enabled at
codegen time. The handler rejects request bodies with properties not defined on the schema.
**Fix**
Remove the extra field from the client payload, or disable `StrictMutate` in `entc.go` and
regenerate if you need to accept forward-compatible clients.
---
### 413 Request Entity Too Large [#413-request-entity-too-large]
**Symptom**
Large POST or PATCH bodies are rejected with HTTP 413.
**Cause**
Default body limit is 8 MiB ([`MaxRequestBodyBytes`](https://entrest.dev/http-handler/configuration#request-body-size)).
**Fix**
Raise the limit in `ServerConfig`, or set it to `-1` to disable checking (not recommended for
public APIs).
---
### List returns 404 instead of empty array [#list-returns-404-instead-of-empty-array]
**Symptom**
`GET /pets?filter=...` returns 404 when no rows match.
**Cause**
[`ListNotFound`](https://entrest.dev/openapi-specs/configuration#mutation-and-list-responses) is enabled. The
default is 200 with an empty `content` array.
**Fix**
Disable `ListNotFound` in `entc.go` if empty results should return 200, or handle 404 in your client
if you enabled it intentionally.
---
## OpenAPI / client generation issues [#openapi--client-generation-issues]
### Enum or schema duplicated in generated client [#enum-or-schema-duplicated-in-generated-client]
**Cause**
Large specs with many filter parameters can produce verbose client code. entrest hoists enums into
`components` to reduce duplication, but filter-heavy schemas still generate many types.
**Fix**
See [Keeping the spec smaller](https://entrest.dev/resources/best-practices#keeping-the-spec-smaller) —
narrow [`WithFilter`](https://entrest.dev/openapi-specs/annotation-reference#withfilter) usage, disable
unused edge endpoints, and eager-load only edges callers need.
---
### Generated client method names are unstable [#generated-client-method-names-are-unstable]
**Cause**
Default `operationId` values follow a predictable pattern (`createUser`, `listUserPets`), but
hand-editing `openapi.json` or conflicting base-spec IDs can break stability.
**Fix**
Set explicit IDs with [`WithOperationID`](https://entrest.dev/openapi-specs/annotation-reference#withoperationid)
on schemas or edges that feed your client generator.
---
## Still stuck? [#still-stuck]
- Compare your setup to the [simple example](https://github.com/lrstanley/entrest/tree/master/_examples/simple)
or [kitchensink example](https://github.com/lrstanley/entrest/tree/master/_examples/kitchensink).
- Validate your spec with tools listed in [Recommended Tools](https://entrest.dev/resources/recommended-tools).
- Open an issue on [GitHub](https://github.com/lrstanley/entrest/issues) with the codegen error,
relevant schema snippets, and your `entc.go` config.