Skip to content

Mount the Handler

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.

Handler code is generated when entrest.Config.Handler is not HandlerNone. Pick the router integration at generation time:

ValueGenerated Handler signature
HandlerStdlibHandler() http.Handler
HandlerChiHandler(r chi.Router)

Set it in your entc.go file:

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 <ent>/rest next to the embedded openapi.json.

Other generation-time options — WithTesting, StrictMutate, pagination defaults, and so on — are documented on entrest.Config and in Runtime Configuration.

After codegen, import your project's <ent>/rest package. The important pieces:

  • rest.NewServer(db, cfg) — builds a *rest.Server from your ent.Client and optional ServerConfig.
  • 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).

Create the server with your database client, then mount the returned handler on your HTTP server.

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.

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.

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.

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.

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:

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 API reference UI
  • GET / — redirects to /docs (stdlib handler only)

Use ?pretty=true on any JSON response to get indented output.

entrest.WithHandler(false) 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.

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.

llms.txtdocumentation for LLMs