Clean Architecture with ent and gqlgen
Writing maintainable code in GraphQL app

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:

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:

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:

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:

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:

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.go
and 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:

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:

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:

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:

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/model
as 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:

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:

You can see that the prefix the 00A
has been attached to the id.
And query the node and it responds successfully:

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:

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.

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

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:

The stack trace looks like this:

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.

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: