GraphQL is a query language for reading and manipulating data for APIs. It prioritizes giving clients or servers the exact data requirement by providing a flexible and intuitive syntax to describe such data.
Compared to a traditional REST API, GraphQL provides a type system to describe schemas for data and, in turn, gives consumers of the API the affordance to explore and request the needed data using a single endpoint.
This post will discuss building a project management application with Golang using the gqlgen library and MongoDB. At the end of this tutorial, we will learn how to create a GraphQL endpoint that supports reading and manipulating project management data and persisting our data using MongoDB.
GitHub repository can be found here.
Prerequisites
To fully grasp the concepts presented in this tutorial, experience with Golang is required. Experience with MongoDB isn’t a requirement, but it’s nice to have.
We will also be needing the following:
- Basic knowledge of GraphQL
- A MongoDB account to host database. Signup is completely free
Let’s code
Getting Started
To get started, we need to navigate to the desired directory and run the command below in our terminal
mkdir project-mngt-golang-graphql && cd project-mngt-golang-graphql
This command creates a project-mngt-golang-graphql folder and navigates into the project directory.
Next, we need to initialize a Go module to manage project dependencies by running the command below:
go mod init project-mngt-golang-graphql
This command will create a go.mod file for tracking project dependencies.
We proceed to install the required dependencies with:
go get github.com/99designs/gqlgen go.mongodb.org/mongo-driver/mongo github.com/joho/godotenv
github.com/99designs/gqlgen is a library for creating GraphQL applications in Go.
go.mongodb.org/mongo-driver/mongo is a driver for connecting to MongoDB.
github.com/joho/godotenv is a library for managing environment variable.
Project Initialization
The gqlgen library uses a schema first approach; it lets us define our APIs using GraphQL’s Schema Definition Language. The library also lets us focus on implementation by generating a project boilerplate.
To generate the project boilerplate, we need to run the command below:
go run github.com/99designs/gqlgen init
The command above generates the following files:
-
gqlgen.ymlA file for configuringgqlgen -
graph/generated/generated.goA file containing all the codesgqlgenautogenerates during execution. We don’t need to edit this file. -
graph/model/models_gen.goA file containing generated models required to build the GraphQL. This file is also autogenerated bygqlgen. -
graph/schema.graphqlsa file for defining our schemas. -
graph/schema.resolvers.goA file to define our application logic. -
server.goThis file is our application entry point.
PS: We might get an error about missing dependencies. We can fix this by reinstalling the packages we installed earlier.
go get github.com/99designs/gqlgen go.mongodb.org/mongo-driver/mongo github.com/joho/godotenv
Setting up MongoDB
With that done, we need to log in or sign up into our MongoDB account. Click the project dropdown menu and click on the New Project button.
Enter the projectMngt as the project name, click Next, and click Create Project..
Click on Build a Database
Select Shared as the type of database.
Click on Create to setup a cluster. This might take sometime to setup.
Next, we need to create a user to access the database externally by inputting the Username, Password and then clicking on Create User. We also need to add our IP address to safely connect to the database by clicking on the Add My Current IP Address button. Then click on Finish and Close to save changes.
On saving the changes, we should see a Database Deployments screen, as shown below:
Connecting our application to MongoDB
With the configuration done, we need to connect our application with the database created. To do this, click on the Connect button
Click on Connect your application, change the Driver to Go and the Version as shown below. Then click on the copy icon to copy the connection string.
Setup Environment Variable
Next, we must modify the copied connection string with the user's password we created earlier and change the database name. To do this, first, we need to create a .env file in the root directory, and in this file, add the snippet below:
MONGOURI=mongodb+srv://<YOUR USERNAME HERE>:<YOUR PASSWORD HERE>@cluster0.e5akf.mongodb.net/<DATABASE NAME>?retryWrites=true&w=majority
Sample of a properly filled connection string below:
MONGOURI=mongodb+srv://malomz:malomzPassword@cluster0.e5ahghkf.mongodb.net/projectMngt?retryWrites=true&w=majority
Load Environment Variable
With that done, we need to create a helper function to load the environment variable using the github.com/joho/godotenv library we installed earlier. To do this, we need to create a configs folder in the root directory; here, create an env.go file and add the snippet below:
package configs
import (
"log"
"os"
"github.com/joho/godotenv"
)
func EnvMongoURI() string {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
return os.Getenv("MONGOURI")
}
The snippet above does the following:
- Import the required dependencies.
- Create an
EnvMongoURIfunction that checks if the environment variable is correctly loaded and returns the environment variable.
Defining our Schema
To do this, we need to navigate the graph folder, and in this folder, update the schema.graphqls file as shown below:
type Owner {
_id: String!
name: String!
email: String!
phone: String!
}
type Project {
_id: String!
ownerId: ID!
name: String!
description: String!
status: Status!
}
enum Status {
NOT_STARTED
IN_PROGRESS
COMPLETED
}
input FetchOwner {
id: String!
}
input FetchProject {
id: String!
}
input NewOwner {
name: String!
email: String!
phone: String!
}
input NewProject {
ownerId: ID!
name: String!
description: String!
status: Status!
}
type Query {
owners: [Owner!]!
projects: [Project!]!
owner(input: FetchOwner): Owner!
project(input: FetchProject): Project!
}
type Mutation {
createProject(input: NewProject!): Project!
createOwner(input: NewOwner!): Owner!
}
The snippet above defines the schema we need for our API by creating two types; a Project and an Owner. We also define Query to perform operations on the types, inputs to define creation properties and Mutation for creating Project and Owner.
Creating application logic
Next, we need to generate logic for our newly created Schema using the gqlgen library. To do this, we need to run the command below in our terminal:
go run github.com/99designs/gqlgen generate
On running the command above, we will get errors about Todo models missing in the schema.resolvers.go file; this is because we changed the default model. We can fix the error by deleting the CreateTodo and Todo functions. Our code should look like the snippet below after the deletion:
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"fmt"
"project-mngt-golang-graphql/graph/generated"
"project-mngt-golang-graphql/graph/model"
)
// CreateProject is the resolver for the createProject field.
func (r *mutationResolver) CreateProject(ctx context.Context, input model.NewProject) (*model.Project, error) {
panic(fmt.Errorf("not implemented"))
}
// CreateOwner is the resolver for the createOwner field.
func (r *mutationResolver) CreateOwner(ctx context.Context, input model.NewOwner) (*model.Owner, error) {
panic(fmt.Errorf("not implemented"))
}
// Owners is the resolver for the owners field.
func (r *queryResolver) Owners(ctx context.Context) ([]*model.Owner, error) {
panic(fmt.Errorf("not implemented"))
}
// Projects is the resolver for the projects field.
func (r *queryResolver) Projects(ctx context.Context) ([]*model.Project, error) {
panic(fmt.Errorf("not implemented"))
}
// Owner is the resolver for the owner field.
func (r *queryResolver) Owner(ctx context.Context, input *model.FetchOwner) (*model.Owner, error) {
panic(fmt.Errorf("not implemented"))
}
// Project is the resolver for the project field.
func (r *queryResolver) Project(ctx context.Context, input *model.FetchProject) (*model.Project, error) {
panic(fmt.Errorf("not implemented"))
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Creating Database Logics
With the GraphQL logic generated, we need to create the code's corresponding database logic. To do this, we need to navigate to the configs folder, here, create a db.go file and add the snippet below:
package configs
import (
"context"
"fmt"
"log"
"project-mngt-golang-graphql/graph/model"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type DB struct {
client *mongo.Client
}
func ConnectDB() *DB {
client, err := mongo.NewClient(options.Client().ApplyURI(EnvMongoURI()))
if err != nil {
log.Fatal(err)
}
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
err = client.Connect(ctx)
if err != nil {
log.Fatal(err)
}
//ping the database
err = client.Ping(ctx, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("Connected to MongoDB")
return &DB{client: client}
}
func colHelper(db *DB, collectionName string) *mongo.Collection {
return db.client.Database("projectMngt").Collection(collectionName)
}
func (db *DB) CreateProject(input *model.NewProject) (*model.Project, error) {
collection := colHelper(db, "project")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
res, err := collection.InsertOne(ctx, input)
if err != nil {
return nil, err
}
project := &model.Project{
ID: res.InsertedID.(primitive.ObjectID).Hex(),
OwnerID: input.OwnerID,
Name: input.Name,
Description: input.Description,
Status: model.StatusNotStarted,
}
return project, err
}
func (db *DB) CreateOwner(input *model.NewOwner) (*model.Owner, error) {
collection := colHelper(db, "owner")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
res, err := collection.InsertOne(ctx, input)
if err != nil {
return nil, err
}
owner := &model.Owner{
ID: res.InsertedID.(primitive.ObjectID).Hex(),
Name: input.Name,
Email: input.Email,
Phone: input.Phone,
}
return owner, err
}
func (db *DB) GetOwners() ([]*model.Owner, error) {
collection := colHelper(db, "owner")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var owners []*model.Owner
defer cancel()
res, err := collection.Find(ctx, bson.M{})
if err != nil {
return nil, err
}
defer res.Close(ctx)
for res.Next(ctx) {
var singleOwner *model.Owner
if err = res.Decode(&singleOwner); err != nil {
log.Fatal(err)
}
owners = append(owners, singleOwner)
}
return owners, err
}
func (db *DB) GetProjects() ([]*model.Project, error) {
collection := colHelper(db, "project")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var projects []*model.Project
defer cancel()
res, err := collection.Find(ctx, bson.M{})
if err != nil {
return nil, err
}
defer res.Close(ctx)
for res.Next(ctx) {
var singleProject *model.Project
if err = res.Decode(&singleProject); err != nil {
log.Fatal(err)
}
projects = append(projects, singleProject)
}
return projects, err
}
func (db *DB) SingleOwner(ID string) (*model.Owner, error) {
collection := colHelper(db, "owner")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var owner *model.Owner
defer cancel()
objId, _ := primitive.ObjectIDFromHex(ID)
err := collection.FindOne(ctx, bson.M{"_id": objId}).Decode(&owner)
return owner, err
}
func (db *DB) SingleProject(ID string) (*model.Project, error) {
collection := colHelper(db, "project")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var project *model.Project
defer cancel()
objId, _ := primitive.ObjectIDFromHex(ID)
err := collection.FindOne(ctx, bson.M{"_id": objId}).Decode(&project)
return project, err
}
The snippet above does the following:
- Imports the required dependencies
- Create a
DBstruct with aclientfield to access MongoDB. - Creates a
ConnectDBfunction that first configures the client to use the correct URI and check for errors. Secondly, we defined a timeout of 10 seconds we wanted to use when trying to connect. Thirdly, check if there is an error while connecting to the database and cancel the connection if the connecting period exceeds 10 seconds. Finally, we pinged the database to test our connection and returned a pointer to theDBstruct. - Creates a
colHelperfunction to create a collection. - Creates a
CreateProjectfunction that takes theDBstruct as a pointer receiver, and returns either the createdProjectorError. Inside the function, we also created aprojectcollection, defined a timeout of 10 seconds when inserting data into the collection, and used theInsertOnefunction to insert theinput. - Creates a
CreateOwnerfunction that takes theDBstruct as a pointer receiver, and returns either the createdOwnerorError. Inside the function, we also created anownercollection, defined a timeout of 10 seconds when inserting data into the collection, and used theInsertOnefunction to insert theinput. - Creates a
GetOwnersfunction that takes theDBstruct as a pointer receiver, and returns either the list ofOwnersorError. The function follows the previous steps by getting the list of owners using theFindfunction. We also read the retuned list optimally using theNextattribute method to loop through the returned list of owners. - Creates a
GetProjectsfunction that takes theDBstruct as a pointer receiver, and returns either the list ofProjectsorError. The function follows the previous steps by getting the list of projects using theFindfunction. We also read the retuned list optimally using theNextattribute method to loop through the returned list of projects. - Creates a
SingleOwnerfunction that takes theDBstruct as a pointer receiver, and returns either the matchedOwnerusing theFindOnefunction orError. - Creates a
SingleProjectfunction that takes theDBstruct as a pointer receiver, and returns either the matchedProjectusing theFindOnefunction orError.
Updating the Application Logic
Next, we need to update the application logic with the database functions. To do this, we need to update the schema.resolvers.go file as shown below:
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"project-mngt-golang-graphql/configs" //add this
"project-mngt-golang-graphql/graph/generated"
"project-mngt-golang-graphql/graph/model"
)
//add this
var (
db = configs.ConnectDB()
)
// CreateProject is the resolver for the createProject field.
func (r *mutationResolver) CreateProject(ctx context.Context, input model.NewProject) (*model.Project, error) {
//modify here
project, err := db.CreateProject(&input)
return project, err
}
// CreateOwner is the resolver for the createOwner field.
func (r *mutationResolver) CreateOwner(ctx context.Context, input model.NewOwner) (*model.Owner, error) {
//modify here
owner, err := db.CreateOwner(&input)
return owner, err
}
// Owners is the resolver for the owners field.
func (r *queryResolver) Owners(ctx context.Context) ([]*model.Owner, error) {
//modify here
owners, err := db.GetOwners()
return owners, err
}
// Projects is the resolver for the projects field.
func (r *queryResolver) Projects(ctx context.Context) ([]*model.Project, error) {
//modify here
projects, err := db.GetProjects()
return projects, err
}
// Owner is the resolver for the owner field.
func (r *queryResolver) Owner(ctx context.Context, input *model.FetchOwner) (*model.Owner, error) {
//modify here
owner, err := db.SingleOwner(input.ID)
return owner, err
}
// Project is the resolver for the project field.
func (r *queryResolver) Project(ctx context.Context, input *model.FetchProject) (*model.Project, error) {
//modify here
project, err := db.SingleProject(input.ID)
return project, err
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
The snippet above does the following:
- Imports the required dependency
- Creates a
dbvariable to initialize the MongoDB usingConnectDBfunction. -
Modifies the
CreateProject,CreateOwner,Owners,Projects,Owner, andProjectfunction using their corresponding function from the database logic.Finally, we need to modify the generated model IDs in the
models_gen.gofile with abson:"_id"struct tags. We use the struct tags to reformat the JSON_idreturned by MongoDB.
//The remaining part of the code goes here
type FetchOwner struct {
ID string `json:"id" bson:"_id"` //modify here
}
type FetchProject struct {
ID string `json:"id" bson:"_id"` //modify here
}
type NewOwner struct {
//code goes here
}
type NewProject struct {
//code goes here
}
type Owner struct {
ID string `json:"_id" bson:"_id"` //modify here
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone"`
}
type Project struct {
ID string `json:"_id" bson:"_id"` //modify here
OwnerID string `json:"ownerId"`
Name string `json:"name"`
Description string `json:"description"`
Status Status `json:"status"`
}
//The remaining part of the code goes here
With that done, we can start a development server using the command below:
go run server.go
Then navigate to 127.0.0.1:8080 on a web browser.
We can also validate the operation on MongoDB.
Conclusion
This post discussed how to build a project management application with Golang using the gqlgen library and MongoDB.
These resources might be helpful:
















Top comments (2)
Very good write up! I was thinking this article include grpc but no I didn't! And thinking how could someone introduce grpc into this design method?
It's something I intend to write about later on. I will keep you posted