{{ .Title }}
{{ .CreatedAt.Format "2006-01-02" }} by {{ .Edges.Author.Name }}
{{ .Body }}
{{- end }}--- title: A beginner's guide to creating a web-app in Go using Ent author: Rotem Tamir authorURL: "https://github.com/rotemtam" authorImageURL: "https://s.gravatar.com/avatar/36b3739951a27d2e37251867b7d44b1a?s=80" authorTwitter: _rtam image: "https://entgo.io/images/assets/cms-blog/share.png" --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; [Ent](https://entgo.io) is an open-source entity framework for Go. It is similar to more traditional ORMs, but has a few distinct features that have made it very popular in the Go community. Ent was first open-sourced by [Ariel](https://github.com/a8m) in 2019, when he was working at Facebook. Ent grew from the pains of managing the development of applications with very large and complex data models and ran successfully inside Facebook for a year before open-sourcing it. After graduating from Facebook Open Source, Ent joined the Linux Foundation in September 2021. This tutorial is intended for Ent and Go novices who want to start by building a simple project: a very minimal content management system. Over the last few years, Ent has become one of the fastest growing ORMs in Go: 
{{ .CreatedAt.Format "2006-01-02" }} by {{ .Edges.Author.Name }}
{{ .Body }}
{{- end }}{{ .CreatedAt.Format "2006-01-02" }} by {{ .Edges.Author.Name }}
``` Finally, we print the post body and close the loop. ```{{ .Body }}
{{- end }} ``` After defining the template, we need to make it available to our program. We embed this template into our binary using the `embed` package ([docs](https://pkg.go.dev/embed)): ```go var ( //go:embed templates/* resources embed.FS tmpl = template.Must(template.ParseFS(resources, "templates/*")) ) ``` #### Server code We continue by defining a type named `server` and a constructor for it, `newServer`. This struct will have receiver methods for each HTTP handler we create and binds the Ent client we created at init to the server code. ```go type server struct { client *ent.Client } func newServer(client *ent.Client) *server { return &server{client: client} } ``` Next, let's define the handler for our blog home page. This page should contain a list of all available blog posts: ```go // index serves the blog home page func (s *server) index(w http.ResponseWriter, r *http.Request) { posts, err := s.client.Post. Query(). WithAuthor(). All(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if err := tmpl.Execute(w, posts); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } ``` Let's zoom in on the Ent code here that is used to retrieve the posts from the database: ```go // s.client.Post contains methods for interacting with Post entities s.client.Post. // Begin a query. Query(). // Retrieve the entities using the `Author` edge. (a `User` instance) WithAuthor(). // Run the query against the database using the request context. All(r.Context()) ``` #### The router To manage the routes for our application, let's use `go-chi`, a popular routing library for Go. ``` go get -u github.com/go-chi/chi/v5 ``` We define the `newRouter` function that sets up our router: ```go // newRouter creates a new router with the blog handlers mounted. func newRouter(srv *server) chi.Router { r := chi.NewRouter() r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Get("/", srv.index) return r } ``` In this function, we first instantiate a new `chi.Router`, then register two middlewares: * `middleware.Logger` is a basic access logger that prints out some information on every request our server handles. * `middleware.Recoverer` recovers from when our handlers panic, preventing a case where our entire server will crash because of an application error. Finally, we register the `index` function of the `server` struct to handle `GET` requests to the `/` path of our server. #### A unit test Before wiring everything together, let's write a simple unit test to check that our code works as expected. To simplify our tests we will install the SQLite driver for Go which allows us to use an in-memory database: ``` go get -u github.com/mattn/go-sqlite3 ``` Next, we install `testify`, a utility library that is commonly used for writing assertions in tests. ``` go get github.com/stretchr/testify ``` With these dependencies installed, create a new file named `main_test.go`: ```go package main import ( "context" "io" "net/http" "net/http/httptest" "testing" _ "github.com/mattn/go-sqlite3" "github.com/rotemtam/ent-blog-example/ent/enttest" "github.com/stretchr/testify/require" ) func TestIndex(t *testing.T) { // Initialize an Ent client that uses an in memory SQLite db. client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") defer client.Close() // seed the database with our "Hello, world" post and user. err := seed(context.Background(), client) require.NoError(t, err) // Initialize a server and router. srv := newServer(client) r := newRouter(srv) // Create a test server using the `httptest` package. ts := httptest.NewServer(r) defer ts.Close() // Make a GET request to the server root path. resp, err := ts.Client().Get(ts.URL) // Assert we get a 200 OK status code. require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) // Read the response body and assert it contains "Hello, world!" body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Contains(t, string(body), "Hello, World!") } ``` Run the test to verify our server works correctly: ``` go test ./... ``` Observe our test passes: ``` ok github.com/rotemtam/ent-blog-example 0.719s ? github.com/rotemtam/ent-blog-example/ent [no test files] ? github.com/rotemtam/ent-blog-example/ent/enttest [no test files] ? github.com/rotemtam/ent-blog-example/ent/hook [no test files] ? github.com/rotemtam/ent-blog-example/ent/migrate [no test files] ? github.com/rotemtam/ent-blog-example/ent/post [no test files] ? github.com/rotemtam/ent-blog-example/ent/predicate [no test files] ? github.com/rotemtam/ent-blog-example/ent/runtime [no test files] ? github.com/rotemtam/ent-blog-example/ent/schema [no test files] ? github.com/rotemtam/ent-blog-example/ent/user [no test files] ``` #### Putting everything together Finally, let's update our `main` function to put everything together: ```go func main() { // Read the connection string to the database from a CLI flag. var dsn string flag.StringVar(&dsn, "dsn", "", "database DSN") flag.Parse() // Instantiate the Ent client. client, err := ent.Open("mysql", dsn) if err != nil { log.Fatalf("failed connecting to mysql: %v", err) } defer client.Close() ctx := context.Background() // If we don't have any posts yet, seed the database. if !client.Post.Query().ExistX(ctx) { if err := seed(ctx, client); err != nil { log.Fatalf("failed seeding the database: %v", err) } } srv := newServer(client) r := newRouter(srv) log.Fatal(http.ListenAndServe(":8080", r)) } ``` We can now run our application and stand amazed at our achievement: a working blog front page! ``` go run main.go -dsn "root:pass@tcp(localhost:3306)/test?parseTime=true" ```  ### Step 4: Adding content :::info You can follow the changes in this step in [this commit](https://github.com/rotemtam/ent-blog-example/commit/2e412ab2cda0fd251ccb512099b802174d917511). ::: No content management system would be complete without the ability, well, to manage content. Let's demonstrate how we can add support for publishing new posts on our blog. Let's start by creating the backend handler: ```go // add creates a new blog post. func (s *server) add(w http.ResponseWriter, r *http.Request) { author, err := s.client.User.Query().Only(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if err := s.client.Post.Create(). SetTitle(r.FormValue("title")). SetBody(r.FormValue("body")). SetAuthor(author). Exec(r.Context()); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } http.Redirect(w, r, "/", http.StatusFound) } ``` As you can see, the handler currently loads the *only* user from the `users` table (since we have yet to create a user management system or login capabilities). `Only` will fail unless exactly one result is retrieved from the database. Next, our handler creates a new post, by setting the title and body fields to values retrieved from `r.FormValue`. This is where Go stores all of the form input passed to an HTTP request. After creating the handler, we should wire it to our router: ```go // newRouter creates a new router with the blog handlers mounted. func newRouter(srv *server) chi.Router { r := chi.NewRouter() r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Get("/", srv.index) // highlight-next-line r.Post("/add", srv.add) return r } ``` Next, we can add an HTML `