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:
| Value | Generated Handler signature |
|---|---|
HandlerStdlib | Handler() http.Handler |
HandlerChi | Handler(r chi.Router) |
Set it in your entc.go file:
//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.Serverfrom yourent.Clientand optionalServerConfig.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 atGET /openapi.jsonby 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.
//nolint:allpackage 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.
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 /usersGET /users/{id}POST /usersPATCH /users/{id}DELETE /users/{id}GET /users/{id}/pets # edge sub-resourceGET /pets/{id}/owner # eager-loaded edge readWhich 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 documentGET /docs— embedded Scalar API reference UIGET /— 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.
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.

