Appwrite + Go: Build APIs without technical overhead
Learn how to abstract technical overhead by leveraging Appwrite to build a project management API in Go
Demola Malomo
Aug 18 2023
9 min read

In every stage of the Sofware Development Lifecycle (SDLC), developers must make strategic decisions around databases, authorization, deployment mechanisms, server sizes, storage management, etc. These decisions must be thoroughly assessed as they can significantly impact the development processes involved in building applications.
One paradigm developers constantly embrace is Backend-as-a-Service (BaaS). Baas abstracts the development overhead associated with SDLC and focuses only on the business logic. It provides developers with server-side capabilities like user authentication, database management, cloud storage, etc.
In this post, we will explore leveraging Appwrite as a BaaS by building a project management API in Go. The API will provide functionalities to create, read, update, and delete a project. The project repository can be found here.
What is Appwrite?
Appwrite is an open-source backend as a service platform that provides sets of APIs and SDKs for building web, mobile, and backend services. The following are some of the benefits of using Appwrite in any application:
- Provides a scalable and robust database
- Realtime functionalities
- Support for serverless functions
- Security certificates and encryption
- Authentication and authorization mechanism
Prerequisites
To follow along with this tutorial, the following are needed:
- Basic understanding of Go
- Appwrite account. Signup is free
Getting started
To get started, we need to navigate to the desired directory and run the command below:
1mkdir go-appwrite && cd go-appwrite
This command creates a Go project called go-appwrite and navigates into the project directory.
Next, we need to initialize a Go module to manage project dependencies by running the command below:
1go mod init go-appwrite
This command will create a go.mod file for tracking the project dependencies.
Finally, we proceed to install the required dependencies with:
1go get github.com/gin-gonic/gin github.com/go-playground/validator/v10 github.com/joho/godotenv
github.com/gin-gonic/gin is a framework for building web applications.
github.com/go-playground/validator/v10 is a library for validating structs and fields.
github.com/joho/godotenv is a library for loading environment variable
Structuring our application
It is essential to have a good project structure as it makes the codebase maintainable and seamless for anyone to read or manage. To do this, we must create an api, cmd, and data folder in our project directory.

api is for structuring our API-related files
cmd is for structuring our application entry point
data is for structuring our application data
Setting up Appwrite
To get started, we need to log into our Appwrite console, click the Create project button, input go_appwrite as the name, and then Create.

Create a Database, Collection, and Add Attributes
Appwrite ships a scalable and robust database that we can use in building our project management API. To do this, first, navigate to the Database tab, click the Create database button, input project as the name, and Create.

Secondly, we need to create a collection for storing our projects. To do this, click the Create collection button, input project_collection as the name, and then click Create.

Lastly, we need to create attributes to represent our database fields. To do this, we need to navigate to the Attributes tab and create attributes for each of the values shown below:
| Attribute key | Attribute type | Size | Required |
|---|---|---|---|
| name | String | 250 | YES |
| description | String | 5000 | YES |

After creating the attributes, we see them as shown below:

Create an API key
To securely connect to Appwrite, we need to create an API key. To do this, we need to navigate to the Overview tab, scroll to the Integrate With Your Server section, and click the API Key button.

Next, input api_go as the name, click the Next button, select Database as the required scope, and Create.

Leveraging Appwrite to build the project management APIs in Go
With our project fully set up on Appwrite, we can now use the database without spinning up any server or managing any other technical overhead.
Set up Environment Variable
To securely connect to our Appwrite provisioned server, Appwrite provides an endpoint and sets of unique IDs that we can use to perform all the required actions. To set up the required environment varaibles, we need to create a .env file in the root directory and add the snippet below:
1API_KEY=<REPLACE WITH API KEY> 2PROJECT_ID=<REPLACE WITH PROJECT ID> 3DATABASE_ID=<REPLACE WITH DATABASE ID> 4COLLECTION_ID=<REPLACE WITH COLLECTION ID>
We can get the required API key and IDs from our Appwrite console as shown below:



Create the API models
Next, we need to create models to represent our application data. To do this, we need to create a model.go file inside the data folder and add the snippet below:
1package data 2 3type Project struct { 4 Id string `json:"$id,omitempty"` 5 Name string `json:"name,omitempty"` 6 Description string `json:"description,omitempty"` 7} 8 9type ProjectRequest struct { 10 Name string `json:"name,omitempty"` 11 Description string `json:"description,omitempty"` 12} 13 14type ProjectResponse struct { 15 Id string `json:"$id,omitempty"` 16 CollectionId string `json:"$collectionId,omitempty"` 17} 18 19type JsonAPIBody struct { 20 DocumentId string `json:"documentId,omitempty"` 21 Data *ProjectRequest `json:"data,omitempty"` 22}
The snippet above creates a Project, ProjectRequest, ProjectResponse, and JsonAPIBody struct with the required properties to describe requests and response types.
Create the API routes
With the models fully set up, we need to navigate to the api folder and create a route.go file for configuring the API routes and add the snippet below:
1package api 2 3import "github.com/gin-gonic/gin" 4 5type Config struct { 6 Router *gin.Engine 7} 8 9func (app *Config) Routes() { 10 //routes will come here 11}
The snippet above does the following:
- Imports the required dependency
- Creates a
Configstruct with aRouterproperty to configure the application methods - Creates a
Routesfunction that takes in theConfigstruct as a pointer
Create the API helpers
With our application models fully set up, we now use them to create our application logic. To do this, we need to create a helper.go file inside the API folder and add the snippet below:
1package api 2 3import ( 4 "log" 5 "net/http" 6 "os" 7 "github.com/gin-gonic/gin" 8 "github.com/go-playground/validator/v10" 9 "github.com/joho/godotenv" 10) 11 12type jsonResponse struct { 13 Status int `json:"status"` 14 Message string `json:"message"` 15 Data any `json:"data"` 16} 17 18func GetEnvVariable(key string) string { 19 err := godotenv.Load() 20 if err != nil { 21 log.Fatal("Error loading .env file") 22 } 23 return os.Getenv(key) 24} 25 26func (app *Config) validateJsonBody(c *gin.Context, data any) error { 27 var validate = validator.New() 28 //validate the request body 29 if err := c.BindJSON(&data); err != nil { 30 return err 31 } 32 //validate with the validator library 33 if err := validate.Struct(&data); err != nil { 34 return err 35 } 36 return nil 37} 38 39func (app *Config) writeJSON(c *gin.Context, status int, data any) { 40 c.JSON(status, jsonResponse{Status: status, Message: "success", Data: data}) 41} 42 43func (app *Config) errorJSON(c *gin.Context, err error, status ...int) { 44 statusCode := http.StatusBadRequest 45 if len(status) > 0 { 46 statusCode = status[0] 47 } 48 c.JSON(statusCode, jsonResponse{Status: statusCode, Message: err.Error()}) 49}
The snippet above does the following:
- Imports the required dependencies
- Creates a
jsonResponsestruct to describe the API response - Creates a
GetEnvVariablefunction that usesgodotenvpackage to load and get environment variable - Creates a
validateBodyfunction that takes in theConfigstruct as a pointer and returns anerror. Inside the function, we validate that requestdataare in the correct format and also use the validator library to validate and check for the required fields - Creates a
writeJSONfunction that takes in theConfigstruct as a pointer and uses thejsonResponsestruct to construct API response when there’s no error - Creates a
errorJSONfunction that takes in theConfigstruct as a pointer and uses thejsonResponsestruct to construct API response when there’s an error
Create the API services
With our application models fully set up, we now use them to create our application logic. To do this, we need to create a service.go file and update it by doing the following:
First, we need to import the required dependencies, create environment variables, and create a function for creating a project.
1package api 2import ( 3 "bytes" 4 "encoding/json" 5 "fmt" 6 "go-appwrite/data" 7 "io/ioutil" 8 "net/http" 9) 10//get details from environment variable 11var projectId = GetEnvVariable("PROJECT_ID") 12var databaseId = GetEnvVariable("DATABASE_ID") 13var collectionId = GetEnvVariable("COLLECTION_ID") 14var apiKey = GetEnvVariable("PROJECT_ID") 15 16func (app *Config) createProject(newProject *data.ProjectRequest) (*data.ProjectResponse, error) { 17 url := fmt.Sprintf("https://cloud.appwrite.io/v1/databases/%s/collections/%s/documents", databaseId, collectionId) 18 19 createdProject := data.ProjectResponse{} 20 jsonData := data.JsonAPIBody{ 21 DocumentId: "unique()", 22 Data: newProject, 23 } 24 postBody, _ := json.Marshal(jsonData) 25 bodyData := bytes.NewBuffer(postBody) 26 27 //making the request 28 client := &http.Client{} 29 req, _ := http.NewRequest("POST", url, bodyData) 30 req.Header.Add("Content-Type", "application/json") 31 req.Header.Add("X-Appwrite-Key", apiKey) 32 req.Header.Add("X-Appwrite-Project", projectId) 33 resp, err := client.Do(req) 34 if err != nil { 35 return nil, err 36 } 37 body, err := ioutil.ReadAll(resp.Body) 38 if err != nil { 39 return nil, err 40 } 41 err = json.Unmarshal([]byte(body), &createdProject) 42 if err != nil { 43 return nil, err 44 } 45 return &createdProject, nil 46}
The snippet above does the following:
- Imports the required dependencies
- Creates required environment variables
- Creates a
createProjectfunction that takes in theConfigstruct as a pointer, and returns either the created project details or an error. The functions also configure the Appwrite’s provisioned server URL by including the required environment variables, adds required headers, and makes a request.
PS: The unique() tag specified when creating a project tells Appwrite to autogenerate the project ID.
Secondly, we need to add a getProject function that uses similar logic as the createProject function to get the details of a project.
1//import goes here 2 3func (app *Config) createProject(newProject *data.ProjectRequest) (*data.ProjectResponse, error) { 4//createProject code goes here 5} 6 7func (app *Config) getProject(documentId string) (*data.Project, error) { 8 url := fmt.Sprintf("https://cloud.appwrite.io/v1/databases/%s/collections/%s/documents/%s", databaseId, collectionId, documentId) 9 10 projectDetail := data.Project{} 11 12 //making the request 13 client := &http.Client{} 14 req, _ := http.NewRequest("GET", url, nil) 15 req.Header.Add("Content-Type", "application/json") 16 req.Header.Add("X-Appwrite-Key", apiKey) 17 req.Header.Add("X-Appwrite-Project", projectId) 18 19 resp, err := client.Do(req) 20 if err != nil { 21 return nil, err 22 } 23 body, err := ioutil.ReadAll(resp.Body) 24 if err != nil { 25 return nil, err 26 } 27 err = json.Unmarshal([]byte(body), &projectDetail) 28 if err != nil { 29 return nil, err 30 } 31 return &projectDetail, nil 32}
Thirdly, we need to add a updateProject function that uses similar logic as the createProject function to update the details of a project.
1//import goes here 2 3func (app *Config) createProject(newProject *data.ProjectRequest) (*data.ProjectResponse, error) { 4//createProject code goes here 5} 6 7func (app *Config) getProject(documentId string) (*data.Project, error) { 8 //getProject code goes here 9} 10 11func (app *Config) updateProject(updatedProject *data.ProjectRequest, documentId string) (*data.ProjectResponse, error) { 12 url := fmt.Sprintf("https://cloud.appwrite.io/v1/databases/%s/collections/%s/documents/%s", databaseId, collectionId, documentId) 13 14 updates := data.ProjectResponse{} 15 16 jsonData := data.JsonAPIBody{ 17 Data: updatedProject, 18 } 19 postBody, _ := json.Marshal(jsonData) 20 bodyData := bytes.NewBuffer(postBody) 21 22 //making the request 23 client := &http.Client{} 24 req, _ := http.NewRequest("PATCH", url, bodyData) 25 req.Header.Add("Content-Type", "application/json") 26 req.Header.Add("X-Appwrite-Key", apiKey) 27 req.Header.Add("X-Appwrite-Project", projectId) 28 resp, err := client.Do(req) 29 if err != nil { 30 return nil, err 31 } 32 body, err := ioutil.ReadAll(resp.Body) 33 if err != nil { 34 return nil, err 35 } 36 err = json.Unmarshal([]byte(body), &updates) 37 if err != nil { 38 return nil, err 39 } 40 return &updates, nil 41}
Lastly, we need to add a deleteProject function that uses similar logic as the createProject function to delete the details of a project.
1//import goes here 2 3func (app *Config) createProject(newProject *data.ProjectRequest) (*data.ProjectResponse, error) { 4//createProject code goes here 5} 6 7func (app *Config) getProject(documentId string) (*data.Project, error) { 8 //getProject code goes here 9} 10 11func (app *Config) updateProject(updatedProject *data.ProjectRequest, documentId string) (*data.ProjectResponse, error) { 12 //updateProject code goes here 13} 14 15func (app *Config) deleteProject(documentId string) (string, error) { 16 url := fmt.Sprintf("https://cloud.appwrite.io/v1/databases/%s/collections/%s/documents/%s", databaseId, collectionId, documentId) 17 18 //making the request 19 client := &http.Client{} 20 req, _ := http.NewRequest("DELETE", url, nil) 21 req.Header.Add("Content-Type", "application/json") 22 req.Header.Add("X-Appwrite-Key", apiKey) 23 req.Header.Add("X-Appwrite-Project", projectId) 24 25 resp, err := client.Do(req) 26 if err != nil { 27 return "", err 28 } 29 _, err = ioutil.ReadAll(resp.Body) 30 if err != nil { 31 return "", err 32 } 33 return documentId, nil 34}
Create the API handlers
With that done, we can use the services to create our API handlers. To do this, we need to create a handler.go file inside api folder and add the snippet below:
1package api 2 3import ( 4 "context" 5 "fmt" 6 "go-appwrite/data" 7 "net/http" 8 "time" 9 "github.com/gin-gonic/gin" 10) 11 12const appTimeout = time.Second * 10 13 14func (app *Config) createdProjectHandler() gin.HandlerFunc { 15 return func(ctx *gin.Context) { 16 _, cancel := context.WithTimeout(context.Background(), appTimeout) 17 var payload data.ProjectRequest 18 defer cancel() 19 20 app.validateJsonBody(ctx, &payload) 21 22 newProject := data.ProjectRequest{ 23 Name: payload.Name, 24 Description: payload.Description, 25 } 26 27 data, err := app.createProject(&newProject) 28 29 if err != nil { 30 app.errorJSON(ctx, err) 31 return 32 } 33 34 app.writeJSON(ctx, http.StatusCreated, data) 35 } 36} 37 38func (app *Config) getProjectHandler() gin.HandlerFunc { 39 return func(ctx *gin.Context) { 40 _, cancel := context.WithTimeout(context.Background(), appTimeout) 41 projectId := ctx.Param("projectId") 42 defer cancel() 43 44 data, err := app.getProject(projectId) 45 46 if err != nil { 47 app.errorJSON(ctx, err) 48 return 49 } 50 51 app.writeJSON(ctx, http.StatusOK, data) 52 } 53} 54 55func (app *Config) updateProjectHandler() gin.HandlerFunc { 56 return func(ctx *gin.Context) { 57 _, cancel := context.WithTimeout(context.Background(), appTimeout) 58 projectId := ctx.Param("projectId") 59 var payload data.ProjectRequest 60 defer cancel() 61 62 app.validateJsonBody(ctx, &payload) 63 newProject := data.ProjectRequest{ 64 Name: payload.Name, 65 Description: payload.Description, 66 } 67 68 data, err := app.updateProject(&newProject, projectId) 69 if err != nil { 70 app.errorJSON(ctx, err) 71 return 72 } 73 74 app.writeJSON(ctx, http.StatusOK, data) 75 } 76} 77 78func (app *Config) deleteProjectHandler() gin.HandlerFunc { 79 return func(ctx *gin.Context) { 80 _, cancel := context.WithTimeout(context.Background(), appTimeout) 81 projectId := ctx.Param("projectId") 82 defer cancel() 83 84 data, err := app.deleteProject(projectId) 85 if err != nil { 86 app.errorJSON(ctx, err) 87 return 88 } 89 90 app.writeJSON(ctx, http.StatusAccepted, fmt.Sprintf("Project with ID: %s deleted successfully!!", data)) 91 } 92}
The snippet above does the following:
- Imports the required dependencies
- Creates a
createdProjectHandler,getProjectHandler,updateProjectHandler, anddeleteProjectHandlerfunctions that returns a Gin-gonic handler and takes in theConfigstruct as a pointer. Inside the returned handler, we defined the API timeout, used the helper functions and the service created earlier to perform the corresponding action.
Update the API routes to use handlers
With that done, we can now update the routes.go file with the handlers as shown below:
1package api 2import "github.com/gin-gonic/gin" 3 4type Config struct { 5 Router *gin.Engine 6} 7 8func (app *Config) Routes() { 9 app.Router.POST("/project", app.createdProjectHandler()) 10 app.Router.GET("/project/:projectId", app.getProjectHandler()) 11 app.Router.PATCH("/project/:projectId", app.updateProjectHandler()) 12 app.Router.DELETE("/project/:projectId", app.deleteProjectHandler()) 13}
Putting it all together
With our API fully set up, we need to create the application entry point. To do this, we need to create a main.go file inside the cmd folder and add the snippet below:
1package main 2 3import ( 4 "go-appwrite/api" 5 "github.com/gin-gonic/gin" 6) 7 8func main() { 9 router := gin.Default() 10 11 //initialize config 12 app := api.Config{Router: router} 13 14 //routes 15 app.Routes() 16 17 router.Run(":8080") 18}
The snippet above does the following:
- Imports the required dependencies
- Creates a Gin router using the
Defaultconfiguration - Initialize the
Configstruct by passing in theRouter - Adds the route and run the application on port
:8080
With that done, we can start a development server using the command below:
1go run cmd/main.go


We can also confirm the project management data by checking the collection on Appwrite.

Conclusion
This post discussed what Appwrite is and provided a detailed step-by-step guide to use it to build a project management API in Go.
These resources may also be helpful:


