Better Programming

Advice for programmers.

Follow publication

Clean Architecture with ent and gqlgen

Manato
Better Programming
Published in
17 min readDec 13, 2021
Photo by Ruth D on Unsplash.

This post will walk you through how to implement a GraphQL app with the Clean Architecture using the ent and the gqlgen package.

Clean Architecture allows us to create a maintainable and testable project by designing the code into several layers with an explicit rule. Rather than introducing the basic concepts of Clean Architecture, this post will highlight how to implement it practically in a GraphQL application. If you want to know the concept before proceeding, take a look at this article.

The ent is an ORM framework for Go and The gqlgen is a library for building GraphQL servers. Both of them are based on a schema-first approach and provide statically typed API using generators. I introduced the integration of these in the previous post so you can check it out.

Example Repository

Here is the final codebase on GitHub:

Folder Structure

The final folder structure is the following:

├── bin
├── cmd
│ ├── app
│ └── migration
├── config
├── docker
├── ent
├── graph
├── pkg
│ ├── adapter
│ │ ├── controller
│ │ ├── handler
│ │ ├── repository
│ │ └── resolver
│ │
│ ├── const
│ ├── entity
│ │ └── model
│ │
│ ├── infrastructure
│ │ ├── datastore
│ │ ├── graphql
│ │ └── router
│ │
│ ├── registry
│ │
│ ├── usecase
│ │ ├── repository
│ │ └── usecase

In the next section, we will set up these step by step.

Implementation Overview

  • Set up a development environment
  • Set up echo
  • Set up ent
  • Set up gqlgen
  • Query
  • Mutation
  • Clean Architecture
  • Handling ULIDs
  • Node Interface
  • Pagination
  • Input filter
  • Handling errors
  • Handling transactions
  • Testing
  • E2E

Set up a development environment

In this section, we will set up these environments:

  • MySQL with docker
  • Initialization of database
  • Config file

MySQL with docker

This post will use MySQL8.0 with docker.

Add docker/docker-compose.yml :

Run it up and you can connect to MySQL.

$ cd docker
$ docker compose up

Initialization of database

To initialize the database in MySQL, create the SQL file in docker/mysql/sql/reset_database.sql like so:

Then, to execute this SQL, create bin/init_db.sh :

This will move ./mysql_data/sql to /var/lib/mysql/ in the docker container and execute the SQL by docker command.

So, run the command:

$ ./bin/init_db.sh

And you will see the database created in MySQL.

To simplify the command, let us create Makefile and add the script to initialize the database.

Create the Makefile at the root project:

Now you can easily do the initialization by running:

$ make setup_db

Config file

To easily access the shared config file, we will introduce the viper package.

Install it first:

$ go get github.com/spf13/viper

And create the config/config.go like so:

This will read the YAML file called ./config.yml and allow you to use it entirely across the application.

Let us add the config/config.yml like this:

This file is intended to be used as a database connection and server port in development mode.

To use this in the app, import the config package and call ReadConfig like so:

config.ReadConfig(config.ReadConfigOption{})fmt.Println(config.C.Server.Address) // 8080

Set up echo

To quickly start off, we will set up a server using the echo package.

Install the packages:

$ go get github.com/labstack/echo/v4
$ go get github.com/labstack/echo/v4/middleware

Add the code in cmd/app/main.go :

Then this will respond successfully at http://localhost:8080:

Hot reloading

The air is a live-reloading package for the Go project. Let us introduce it to our app to develop faster.

Install it:

$ go install github.com/cosmtrek/air@v1.27.3

And create the .air.toml at the root project and write this:

This configuration allows us to watch new changes and re-build automatically.

Run the command:

$ air

Then, you can see the log like so:

air

To simplify the command, let us add the script to start the server.

Open up the Makefile and add the script:

Now that we can start the server by running this:

$ make start

Set up ent

Next, we will introduce the ent package to our application and configure the database.

In our application, let’s suppose that these tables will be created:

  • users — the user can have multiple todos
  • todos — the todos can have one user

The relation of this two is a one-to-many relationship.

Install ent

Let us install it:

$ go get -d entgo.io/ent/cmd/ent

Create a user schema

First, we will create a user schema.

Run this:

$ ent init User

Then, the ent directory should look like this:

ent
├── generate.go
└── schema
└── user.go

Open up the ent/schema/user.go and add some fields as follows:

And run the generator:

$ go generate ./ent

Database Migration

Now that we’re ready to migrate our schema to the database.

In order to run the migration, create the cmd/migration/main.go and write this:

Add the migration script to the Makefile :

Then, run this:

$ make migrate_schema

After the migration, you can see the users table created in the database:

users table

Create a todo schema

Next, we will add a todo schema to our application and connect it to the users edges.

First, we will create a todo schema by running this:

$ ent init Todo

And you can see the todo schema file created:

ent/schema
├── todo.go
└── user.go

Open the ent/schema/todo.go and add some fields:

To connect the user schema, add edge type to Edges :

To configure the edges, open up the ent/schema/user.go and add edge like so:

Now that the todo table has the user_id as foreign-key.

So, run the generator:

$ go generate ./ent

And migrate it:

$ make migrate_schema

Then, the todos table is created as follows:

todos table

NOTE: ent makes foreign-key nullable by default. After the PR is merged, you can set NOT NULL to foreign-key as required edge.

Schema description

The ent provide ent describe to get a description of your graph schema.

Run this:

$ ent describe ./ent/schema

And the output is like this:

ent shema description

Set up gqlgen

To integrate GraphQL into the ent package, the gqlgen package is available, and entgql package is provided as a plugin.

First, install the gqlgen:

$ go get github.com/99designs/gqlgen

To set up the gqlgen, run this initialization:

$ gqlgen init

And this will generate the following layouts under the root project:

├── gqlgen.yml
├── graph
│ ├── generated
│ │ └── generated.go
│ ├── model
│ │ └── models_gen.go
│ ├── resolver.go
│ ├── schema.graphqls
│ └── schema.resolvers.go

To start the GraphQL server, open up the cmd/app/main.go and write the code:

Then you can see the playground page at http://localhost:8080/playground:

GraphQL Playground

Connect ent to gqlgen

Install a plugin package first:

$ go get entgo.io/contrib/entgql

In order to use gqlgen in the ent project, the ent extension needs to be enabled to our project.

To do that, create a new file named, ent/entc.goand add this code:

And then, open up ent/generate.go and change the code to:

Run the generator:

$ go generate ./ent

Next, in order to use the ent.Client in gelgen resolver, open up the graph/resolver.go and pass it to the schema as an additional dependency:

And pass the ent.Cleint to graph.NewSchema in the cmd/app/main.go :

Query

We’ve now completed the GraphQL configuration. In the next step, let’s create a query and try to retrieve data with the ent client.

User query

First, open up the graph/schema.graphqls and change to this:

And create graph/user.graphqls :

In order to bind the User type to the ent model, the autobind option needs to be changed in gqlgen.yml like so:

And comment out the model option:

And run the generator:

$ gqlgen

Open up the graph/user.resolvers.go and write this:

In the playground, run the user query and you can see the data responded:

Playground

Todo query

Let us add a todo query to our app in the next step.

Create graph/todo.graphqls :

The TestTodoStatus needs to be bound to the ent model so, open up the gqlgen.yml and add the TodoStatus to the models option:

Then, run the generator:

$ gqlgen

Open up the graph/todo.resolvers.go and write this:

And the query result should look like this:

todo query

Now that we’ve implemented the todos query, so we can include it in the user query.

Open up the graph/user.graphqls and add the todos to the user query:

Then, run the generator:

$ gqlgen

Query a user and the result should look as follows:

user query

Mutation

In the next step, we will implement the mutation with the ent client.

First, add the mutation type in graph/schema.graphqls :

And open up the graph/user.graphqls and add the mutation type like so:

In order to bind the CreateUserInput and UpdateUserInput to the gqlgen, the model needs to be created in the ent package.

To automatically bind the model, we will use the template feature provided by the ent package.

Create ent/templates/mutation_input.tmpl and paste this:

Open up the ent/entc.go and add the option like so:

Then, run the generator:

$ go generate ./ent

After that, ent/mutation_input.go has been generated with the CreateTodoInput and UpdateTodoInput type like so:

We’re ready to bind these to the gqlgen so run this:

$ gqlgen

And add the resolvers to the graph/user.resolvers.go :

Execute the createUser in the playground, the result should look like this:

Playground

Clean Architecture

To integrate Clean Architecture into our app, the four layers will be introduced and matched with the folders as follows.

  • Entities Layer — entity
  • Use cases Layer — usecase
  • Interface Adapters Layer — adapter
  • Frameworks & Drivers Layer — infrastructure
pkg
├── adapter
│ ├── controller # Controller
│ ├── repository # Specific implementaion of repository
│ └── resolver # GraphQL resolvers


├── entity
│ └── model # Entity of model, (e.g. ent.User, ent.Todo)


├── infrastructure
│ ├── datastore # MySQL configuration
│ ├── graphql # GrahpQL configuration
│ └── router # Echo router


├── usecase
│ ├── repository # Interface for adapter
│ └── usecase # Usecase for application logic

Entities Layer

The entities layer is supposed to include domain models of the entire application. In our application, the ent package is responsible for that, but we don’t want to import the ent package in every layer and to let them depend on specific technologies, which leads to violates the Dependency rules. The impact of changing technology on the codebase needs to be minimized.

To do that, the ent package needs to be imported only in the entity/model. Then, other layers can import them from the entity/modelas domain model.

So let us create pkg/entity/model/user.go :

To use existing methods and struct types, the model needs to be defined as an alias declaration instead of an embedded struct or type definition.

Note: Embedded struct which allows you to extend methods in Go, could be used, but in some cases, the gqlgen generator can not conform to its type.

For todo, create pkg/entity/model/todo.go :

Use cases Layer

The use cases layer has two directories:

  • repository
  • usecase

repository

The repository directory is providing interfaces of CRUD API for the entity models. This is intended to be used in the usecase folder.

Create pkg/usecase/repository/user.go :

usecase

In the usecase folder, create pkg/usecase/usecase/user.go and call the interface:

Interface Adapter Layer

The interface adapter layer has three directories:

  • controller
  • repository
  • resolver

controller

The controller directory is intended to be used from the GraphQL resolver and calls the usecase package.

First, create pkg/adapter/controller/controller.go :

And create pkg/adapter/controller/user.go :

repository

In the repository folder, the specific implementations of the use case's repository will be included and conformed to its type. To persist to the database, the ent package will be used.

Create pkg/adapter/repository/user.go :

resolver

The resolver directory includes a set of resolvers generated by the gqlgen command. In order to move the files from graph to pkg/adapter/resolver, open up the gqlgen.yml and modify the resolver section like so:

And run the generator:

$ gqlgen

And the directory should look like this:

pkg/adapter/resolver
├── resolver.go
├── schema.resolvers.go
├── todo.resolvers.go
└── user.resolvers.go

Registry

To use the ent client in pkg/adapter/repository, initialize the functions in each layer and pass it down.

First, create pkg/registry/registry.go :

This is a root registry function that generates all controllers.

Next, create pkg/registy/user.go :

This passes the ent client to the pkg/adapter/repository and conform to the interface of the repository in pkg/usecase/repositry.

Use controller in resolver

In order to use the controller from the resolver function, pass it to the resolver initialization.

Open up the pkg/adapter/resolver/resolver.go and add the controller like so:

Then, open up the pkg/adapter/resolver/user.resolvers.go and call the controller in the resolver function:

Frameworks and Drivers Layer

The frameworks and drivers layer has three directories:

  • datastore
  • graphql
  • router

datastore

The datastore directory has code related to initializing the database and the client.

Create pkg/infrastructure/datastore/datastore.go :

graphql

The graphql directory is responsible for generating a graphql server.

Create pkg/infrastructure/graphql/graphql.go :

router

The router directory has an implementation of an HTTP router.

Create pkg/infrastructure/router/router.go :

Main function

We’ve finished organizing each layer, so finally, open up the pkg/cmd/app/main.go and write the initialization as follows:

Organize Create and Update functions

Similar to the steps above, the create and update functions need to be organized according to the Clean Architecture.

You can see the changes below:

Handling ULIDs

In the ent package, int type is used by default as the id field in databases and auto-incremented. For some applications, the incremented number needs to be avoided, and it may be preferable to use UUIDs or ULIDs instead. Since we are using MySQL in our application, ULIDs, which are sortable IDs, are preferred to be used as primary-key due to performance issues.

Thanks to the contribution of the PR, the ent package has its example with ULIDs.

So, let’s follow the example to implement the ULIDs in our application.

Add

First, install the ULIDs package for Go:

$ go get github.com/oklog/ulid/v2

Change ID field to ULIDs

Create ent/schema/ulid/ulid.go :

In order to configure the id field to be customized, open up the ent/schema/user.go and add the id type to the Fields like so:

The GoType is used to be convertible to the Go basic type or the type that implements the ValueScanner interface.

And open up ent/schema/todo.go and add the type as follows:

The user_id needs to be changed to the ulid type as well.

Then, run the generator:

$ go generate ./ent

Since the ent migration does not support changing the primary key of tables, so we need to change or delete the table before migrating the schema like so:

alter table users modify id varchar(255);
alter table todos modify id varchar(255);

Run the migration:

$ make migrate_schema

Bind ULIDs to gqlgen

Next, let’s change the ID model of gqlgen and bind it to the ulid.

In order to use the ulid type under the pkg directory, create an ID type to pkg/entity/model/id.go :

Then, open up the gqlgen.yml and bind the type to ID model:

Run the generator:

$ gqlgen

After that, pkg/adapter/resolver/user.resolvers.go should look like this:

You can see the id parameter has been changed from int to the ulid.ID, so let us change the id type in all layers.

pkg/adapter/controller/user.go :

pkg/usecase/usecase/user.go :

pkg/usecase/repository/user.go :

pkg/adapter/repository/user.go :

After that, run the mutation and the response should look like this:

ULIDs

Node Interface

The Node Interface is a standard way in GraphQL, which provides data by querying on the root with a single ID.

An example looks like this:

{
node(id: "4") {
id
... on User {
name
}
}

The ent package supports the Node interface through its GraphQL integration, so let’s implement it.

Add Node interface to GraphQL

First, open up the graph/schema.graphqls and add the Node interface and the node query:

And open up the graph/user.graphqls and add the interface to the User:

Next, create the Node model to pkg/entity/model/node.go :

And bind it to the Node model in the gqlgen.yml .

Then, run the generator:

$ gqlgen

After the generation, the resolver for Node has been created in pkg/adapter/resolver/schema.resolvers.go :

Map ID to Tables

In the Node interface, the GraphQL server has to respond to data based on the ID sent to. To do this, we need to identify which table the ID is contained in.

In our application, ULID is used as the primary key. By adding a prefix to it, we can detect which table it is.

For instance, we can use 0AA for the users table and 0AB for the todos table like this:

// users table
0AA01FPBT4QKAA...
0AA01FPBT4QKAA...
0AA01FPBT4QKAA...
// todos table
0AB01FPBT4QKAA...
0AB01FPBT4QKAA...
0AB01FPBT4QKAA...

So, let us implement this. Create pkg/const/globalid/globalid.go and write like this:

This globalid package handles a map object of the id and tables and provides a function that identifies which table the ID is contained in.

And next, add a prefix to the user schema and the todo schema.

Open up the ent/schema/user.go and add a prefix to the MustNew function:

The todo schema as well:

The MustNew function automatically adds the prefix to ULID.

And next, create ent/ulid.go :

This is intended to be used in the resolvers function and returns the table type.

Implement resolver

Now that we’re ready to serve the data from the Node resolver.

Open up the pkg/adapter/resolver/schema.resolvers.go and write this:

The ent.WithNodeType sets the node type resolver function which maps the ID to tables.

So, let’s test it out.

First, create the user like so:

Node Interface

You can see that the prefix the 00A has been attached to the id.

And query the node and it responds successfully:

Node Interface

Pagination

When handling pagination in GraphQL, the Relay Cursor Connections pattern is preferable to be implemented. The ent package supports this specification through GraphQL integration.

Add Graphql type

First, add some types to the GrahpQL file.

Open up the graph/schema.graphqls and add Cursor and PageInfo type:

And next, open up the graph/user.graphqls and add the connection type:

Bind model

We’ve created the Cursor and PageInfo type in GraphQL file. In order to configure these in the gqlgen, we need to create the type for them in pkg/entity/model.

Create pkg/entity/model/pagination.go :

And open up the pkg/entity/model/user.go and add the type:

Then, run the generator:

$ gqlgen

After the generation, the Users resolver function has been created in the pkg/adapter/resolver/user.resolvers.go :

Implement pagination

Let’s implement pagination in our application then.

First, create the List interface in the pkg/usercase/repository/user.go :

And then, call the interface in the pkg/usecase/usecase/user.go like so:

And next, call the usecase n the pkg/adapter/controller/user.go :

Next, add the paging implementation to the pkg/adapter/repository/user.go :

Lastly, call the controller from the resolver function in the pkg/adapter/resolver/user.resolvers.go :

We’ve done the pagination, so let’s test it out. The result should look like this:

Playground

Filter inputs

Next, we will add more conditions to our input of GraphQL mutations. The ent package allows us to use type-safe GraphQL filters through its generation.

configure ent

To configure it in the ent, open up the ent/entc.go file and add these extensions:

And run the generation:

$ go generate ./ent

This has created graph/ent.graphql which defined the types related to input filters.

Configure gqlgen

Open up the graph/user.graphqls and add the where: UserWhereInput to the users query:

To bind the model, add the type to pkg/entity/model/user.go :

And run the generator:

$ gqlgen

After the generation, you can see the where parameter has been added to the Users resolver at pkg/adapter/resolver/user.resolvers.go :

Implement filter input

Open up the pkg/usecase/repository/user.go add the parameter to the List interface:

And add it to the pkg/usecase/usecase/user.go as well:

Next, open up the pkg/adapter/controller/user.go and add the where parameter to the List interface:

Open up the pkg/adapter/repository/user.go and add the filter to the List method:

Lastly, pass the parameter to the controller at the pkg/adapter/resolver/user.resolvers.go :

In the playground, you can see the filter list in the docs.

Input filters

If you need a list of users whose age is 30 years or older, you can write it like this:

Playground

Handling errors

All resolvers generated in the gqlgen should return an error to be sent to the end-users. To return multiple errors, you can use the graphql.AddError functions like so:

graphql.AddError(ctx, gqlerror.Errorf("error1!"))
graphql.AddError(ctx, gqlerror.Errorf("error2!"))

Ane the errors of the response should look like this:

{
"data": {
"todo": null
},
"errors": [
{ "message": "error1!", "path": [ "todo" ] },
{ "message": "error2!", "path": [ "todo" ] },
]
}

They will be returned in the same order as called.

In our application, we will create a model called error, which handles all the errors caught and responds appropriately to the end-users. Also, make sure to output the stack trace.

For the stack trace, we will introduce github.com/pkg/errors.

$ go get github.com/pkg/errors@v0.9.1

And create the pkg/entity/model/error.go like so:

This is intended to be called when returns an error in any layer. If you handle a database error, then it needs to be called in the pkg/adapter/repository/user.go like so:

The under the layer of the adapter, it can be called multiple times because it wraps an error and returns a new one.

To handle the errors before responding, we will create the pkg/adapter/handler/error.go :

This unwraps all errors and adds them to the response through the graphql.AddError function.

Let’s apply this to the resolver. Open up pkg/adapter/resolver/user.resolvers.go and add the handler to the CreateUser:

Then, if the size of the name is over, it will respond like this:

Playground

The stack trace looks like this:

Stack trace

Handling transactions

The ent package allows us to execute each GraphQL mutation in one database transaction by automatically wrapping the mutations with a transaction, which commits at the end or roll back it in case of a GrahpQL error.

To configure that, open up the pkg/infrastructure/graphql/graphql.go and add the option:

Create the pkg/repository/with_transactional_mutation.go :

Suppose that the CreateWithTodo function creates a user and a todo in a transaction, we can write the following:

If it fails to create a user, successful rollback the transaction.

Rollback

Testing

In our application, we will use a database to test the repository instead of mocking the interface.

Set up database

First, create the docker/mysql_date/sql/reset_database.test.sql :

And next, create the bin/init_db_test.sh :

And run this:

$ ./bin/init_db_test.sh

Set up environment

In order to configure the database, create the testutil/config.go :

And create the testutil/database.go :

Implement testing

To simplify the test, we will write it through the Table Driven Test and the AAA (Arrange Act Assert) pattern.

Create the pkg/adapter/repository/user_test.go and write this:

In the arrange section, the three users have been prepared in the database. Then, in the act section, call the List function from the repository and assert the result.

go test ./pkg/adapter/repository/...                                                                                                                                                         ok   golang-clean-architecture-ent-gqlgen/pkg/adapter/repository 0.556s

E2E

The httpexpect is an end-to-end HTTP and API testing for Go. This is basically intended to be used for Rest API, not GraphQL. But actually, to test the GraphQL server, all you need to do is to use the POST method and send a query.

Install httpextect

Install the httpextect:

$ go get github.com/gavv/httpexpect/v2

Set up database

Again, we will prepare a database for E2E testing first.

Create the docker/mysql_date/sql/reset_database.e2e.sql :

And add the bin/init_db_e2e.sh :

Then, run the script:

./bin/init_db_e2e.sh

Set up environment

In order to configure the database, add the ReadConfigE2E to the testutil/config.go :

Create the testutil/e2e/e2e.go and add some utility functions:

Implement testing

Create the test/e2e/mutation/user_test.go :

To send a query to the GraphQL server, use the POST method and add a query as a string like this:

Then run the test:

$ go test ./test/e2e/...ok   golang-clean-architecture-ent-gqlgen/test/e2e/mutation 0.525s

Conclusion

As you can see, using the ent and gqlgen package, which are strongly type-safe API makes your application solid and maintainable. In addition, structuring the Clean Architecture makes it more flexible and testable.

You can view the final code here:

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Responses (2)

Write a response