Appwrite + Go: Build APIs without technical overhead

Learn how to abstract technical overhead by leveraging Appwrite to build a project management API in Go

avatar

Demola Malomo

Aug 18 2023

9 min read

avatar

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.

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 project

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.

Create database

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.

Create collection

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 keyAttribute typeSizeRequired
nameString250YES
descriptionString5000YES

Create attribute create

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

List of attributes

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.

Create API key

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

input  name create Set permission

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:

API key

Project ID

Database and Collection ID

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 Config struct with a Router property to configure the application methods
  • Creates a Routes function that takes in the Config struct 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 jsonResponse struct to describe the API response
  • Creates a GetEnvVariable function that uses godotenv package to load and get environment variable
  • Creates a validateBody function that takes in the Config struct as a pointer and returns an error. Inside the function, we validate that request data are in the correct format and also use the validator library to validate and check for the required fields
  • Creates a writeJSON function that takes in the Config struct as a pointer and uses the jsonResponse struct to construct API response when there’s no error
  • Creates a errorJSON function that takes in the Config struct as a pointer and uses the jsonResponse struct 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 createProject function that takes in the Config struct 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, and deleteProjectHandler functions that returns a Gin-gonic handler and takes in the Config struct 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 Default configuration
  • Initialize the Config struct by passing in the Router
  • 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

create update

Get details Delete

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

Detail

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:

Related posts