Skip to content
Documentation & entrest itself are a work in progress (expect breaking changes). Check out the GitHub Project to contribute.

Getting Started

entrest is an EntGo extension for generating compliant OpenAPI specs and an HTTP handler implementation that matches that spec. It expands upon the approach used by 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!

Project Structure

We recommend a project structure that looks similar to this:

  • Directoryinternal
    • ...
    • Directorydatabase
      • Directoryent/ contains all entgo-generated code
        • ...
        • Directoryrest/ HTTP handler, OpenAPI spec & entrest code
      • Directoryschema/ 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, including pinning codegen dependencies.
  • main.go main entrypoint for the application, mount HTTP handler, custom endpoints, etc
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 you 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

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

You can find a full example project that matches this guide here.

  1. Start by initializing a new Go project:

    Terminal window
    mkdir -p my-project && cd my-project
    go mod init github.com/example/my-project
  2. Setup subfolder for schema files:

    Terminal window
    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 of the attached annotations:

    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:

    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.

    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. This file is also used to pin codegen dependencies:

    internal/generate.go
    package internal
    //go:generate go run -mod=readonly database/entc.go
    import (
    // Import tools that are used in "go:build ignore" based files, which won't
    // automatically be tracked in go.mod.
    _ "entgo.io/ent/entc/gen"
    _ "github.com/lrstanley/entrest"
    _ "github.com/ogen-go/ogen"
    )
  6. Add a main.go file, which mounts the HTTP handler, and initializes the database client:

    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)
    }
  7. Lastly, run code generation:

    Terminal window
    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.
  2. A fully-functional HTTP handler implementation using 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

To run the application, simply run the following commands to download all dependencies, then run the application:

Terminal window
go mod tidy
go run main.go
[...]
go: downloading github.com/go-playground/assert/v2 v2.0.1
[...]
running http server

Next Steps

Now that you've got a basic HTTP API setup, you can start making API requests to it, or further customizing it.