Create a GraphQL-powered project management endpoint in Golang and MongoDB
Tags: graphql, go, mongodb, gqlgen
Published: 2022-07-25 09:14:15

---
title: Create a GraphQL-powered project management endpoint in Golang and MongoDB
published: true
description: This post will discuss building a project management application with Golang using the gqlgen library and MongoDB.
tags: #GraphQL #Golang #MongoDB #gqlgen
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cc7z8kzh6wfg2fp8tsxw.png
# Use a ratio of 100:42 for best results.
---

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](https://github.com/99designs/gqlgen) library and [MongoDB](https://www.mongodb.com/). 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](https://github.com/Mr-Malomz/project-mngt-golang-graphql-gin).

# 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](https://www.mongodb.com/) to host database. [**Signup**](https://www.mongodb.com/cloud/atlas/register) **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 

```bash
    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:

```bash
    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:

```bash
    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](https://github.com/99designs/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:
```bash
    go run github.com/99designs/gqlgen init
```

The command above generates the following files:

- `gqlgen.yml` A file for configuring `gqlgen`
- `graph/generated/generated.go` A file containing all the codes `gqlgen` autogenerates during execution. We don’t need to edit this file.
- `graph/model/models_gen.go` A file containing generated models required to build the GraphQL. This file is also autogenerated by `gqlgen`.
- `graph/schema.graphqls` a file for defining our schemas.
- `graph/schema.resolvers.go` A file to define our application logic.
- `server.go` This 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.*

```bash
    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](http://) account. Click the project dropdown menu and click on the **New Project** button.

![New Project](https://paper-attachments.dropbox.com/s_B58C50BB61437E91E681B5825981F54733906EC9A6070DFB4A21109C66F0E3FA_1637365380025_Screenshot+2021-11-20+at+00.33.27.png)


Enter the `projectMngt` as the project name, click **Next**, and click **Create Project..**

![enter project name](https://paper-attachments.dropbox.com/s_5DB793BEFC159F569DA013C68231051E6736BBB84507D7CF5CD2A8277968E59C_1657388688286_Screenshot+2022-07-09+at+18.44.22.png)
![Create Project](https://paper-attachments.dropbox.com/s_0E637C74C0D58C5FC3FBA1D21E2CF4F11E7592B7929DAE10790A9D12FE972D1D_1640511449329_Screenshot+2021-12-26+at+10.36.27.png)


Click on **Build a Database**

![](https://paper-attachments.dropbox.com/s_0E637C74C0D58C5FC3FBA1D21E2CF4F11E7592B7929DAE10790A9D12FE972D1D_1640511574285_Screenshot+2021-12-26+at+10.38.59.png)


Select **Shared** as the type of database.

![Shared highlighted in red](https://paper-attachments.dropbox.com/s_B58C50BB61437E91E681B5825981F54733906EC9A6070DFB4A21109C66F0E3FA_1637334670330_Screenshot+2021-11-19+at+15.59.38.png)


Click on **Create** to setup a cluster. This might take sometime to setup.

![Creating a cluster](https://paper-attachments.dropbox.com/s_B58C50BB61437E91E681B5825981F54733906EC9A6070DFB4A21109C66F0E3FA_1637334961608_Screenshot+2021-11-19+at+16.13.28.png)


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.

![Create user](https://paper-attachments.dropbox.com/s_0E637C74C0D58C5FC3FBA1D21E2CF4F11E7592B7929DAE10790A9D12FE972D1D_1640512109914_Screenshot+2021-12-26+at+10.43.36.png)
![Add IP](https://paper-attachments.dropbox.com/s_0E637C74C0D58C5FC3FBA1D21E2CF4F11E7592B7929DAE10790A9D12FE972D1D_1640512118747_Screenshot+2021-12-26+at+10.43.52.png)


On saving the changes, we should see a Database Deployments screen, as shown below:

![Database Screen](https://paper-attachments.dropbox.com/s_0E637C74C0D58C5FC3FBA1D21E2CF4F11E7592B7929DAE10790A9D12FE972D1D_1640512328028_Screenshot+2021-12-26+at+10.51.34.png)

## 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

![Connect to database](https://paper-attachments.dropbox.com/s_0E637C74C0D58C5FC3FBA1D21E2CF4F11E7592B7929DAE10790A9D12FE972D1D_1640512936968_Screenshot+2021-12-26+at+10.51.34.png)


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.

![connect application](https://paper-attachments.dropbox.com/s_0E637C74C0D58C5FC3FBA1D21E2CF4F11E7592B7929DAE10790A9D12FE972D1D_1640513302462_Screenshot+2021-12-26+at+11.03.40.png)
![Copy connection string](https://paper-attachments.dropbox.com/s_0E637C74C0D58C5FC3FBA1D21E2CF4F11E7592B7929DAE10790A9D12FE972D1D_1640513302542_Screenshot+2021-12-26+at+11.04.29.png)


**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:

```bash
    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:
```bash
    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:

```go
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 `EnvMongoURI` function 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:
```graphql
    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, `input`s 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
    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:
```go
    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:
```go
    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 `DB` struct with a `client` field to access MongoDB.
- Creates a `ConnectDB` function 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 the `DB` struct.
- Creates a `colHelper` function to create a collection.
- Creates a `CreateProject` function that takes the `DB` struct as a pointer receiver, and returns either the created `Project` or `Error`. Inside the function, we also created a `project` collection, defined a timeout of 10 seconds when inserting data into the collection, and used the `InsertOne` function to insert the `input`.
- Creates a `CreateOwner` function that takes the `DB` struct as a pointer receiver, and returns either the created `Owner` or `Error`. Inside the function, we also created an `owner` collection, defined a timeout of 10 seconds when inserting data into the collection, and used the `InsertOne` function to insert the `input`.
- Creates a `GetOwners` function that takes the `DB` struct as a pointer receiver, and returns either the list of `Owners` or `Error`. The function follows the previous steps by getting the list of owners using the `Find` function. We also read the retuned list optimally using the `Next` attribute method to loop through the returned list of owners. 
- Creates a `GetProjects` function that takes the `DB` struct as a pointer receiver, and returns either the list of `Projects` or `Error`. The function follows the previous steps by getting the list of projects using the `Find` function. We also read the retuned list optimally using the `Next` attribute method to loop through the returned list of projects. 
- Creates a `SingleOwner` function that takes the  `DB` struct as a pointer receiver, and returns either the matched `Owner` using the `FindOne` function or `Error`.
- Creates a `SingleProject` function that takes the  `DB` struct as a pointer receiver, and returns either the matched `Project` using the `FindOne` function or `Error`.

**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:

```go
    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 `db`  variable to initialize the MongoDB using `ConnectDB` function.
- Modifies the `CreateProject`, `CreateOwner`, `Owners`, `Projects`, `Owner`, and `Project` function using their corresponding function from the database logic.


    Finally, we need to modify the generated model IDs in the `models_gen.go` file with a `bson:"_id"` struct tags. We use the struct tags to reformat the JSON `_id` returned by MongoDB.

```go
    //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:
```bash
    go run server.go
```

Then navigate to `127.0.0.1:8080` on a web browser.

![server running](https://paper-attachments.dropbox.com/s_5DB793BEFC159F569DA013C68231051E6736BBB84507D7CF5CD2A8277968E59C_1657413806519_Screenshot+2022-07-10+at+01.43.16.png)



![create owner](https://paper-attachments.dropbox.com/s_5DB793BEFC159F569DA013C68231051E6736BBB84507D7CF5CD2A8277968E59C_1657413918204_Screenshot+2022-07-10+at+01.21.51.png)
![Create project](https://paper-attachments.dropbox.com/s_5DB793BEFC159F569DA013C68231051E6736BBB84507D7CF5CD2A8277968E59C_1657413918217_Screenshot+2022-07-10+at+01.24.30.png)


We can also validate the operation on MongoDB.

![MongoDB database](https://paper-attachments.dropbox.com/s_5DB793BEFC159F569DA013C68231051E6736BBB84507D7CF5CD2A8277968E59C_1657414011601_Screenshot+2022-07-10+at+01.28.11.png)

# Conclusion

This post discussed how to build a project management application with Golang using the gqlgen library and MongoDB.

These resources might be helpful:

- [GraphQL official page](https://graphql.org/)
- [gqlgen GraphQL library](https://github.com/99designs/gqlgen)
- [MongoDB Go Driver](https://docs.mongodb.com/drivers/go/current/)
- [Build a REST API with Golang and MongoDB](https://dev.to/hackmamba/build-a-rest-api-with-golang-and-mongodb-gin-gonic-version-269m)