entrest

Getting Started

A guide to getting started with entrest.

Tutorial

Step-by-step setup for a new entrest project — schemas, codegen, and a running HTTP server. Assumes basic familiarity with EntGo.

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!

It is recommended to read through the EntGo getting started guide before proceeding to get a better understanding of EntGo.

Additionally, to load extensions with additional configuration, using EntGo's entc integration is required.

Project Structure

We recommend a project structure that looks similar to this:

├── 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, 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 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

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.

Start by initializing a new Go project:

mkdir -p my-project && cd my-project
go mod init github.com/example/my-project

Setup subfolder for schema files:

mkdir -p internal/database/schema

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:

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),
            ),
    }
}

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)
    }
}

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"
)

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)
}

Lastly, run code generation:

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:

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.

Last updated on

On this page