Build APIs faster with Go and Appwrite

This post discusses how to quickly build REST APIs with the Appwrite Go SDK, eliminating the need to manually configure headers for API keys and project IDs, switch URLs for different endpoints, handle requests, and convert responses to JSON.

avatar

Demola Malomo

Dec 10 2024

6 min read

avatar

Appwrite Init: a week-long program dedicated to showcasing and celebrating everything new with Appwrite. The 2024 edition saw the launch of Appwrite 1.5, introducing exciting features such as:

  • Local development support for serverless functions.
  • A new and better Appwrite command line interface (CLI).
  • Improved Appwrite serverless function.
  • Go runtime support and SDK
  • Mock numbers to test phone authentication.

The addition of Go runtime support and SDK stood out to me in particular. Developers can now build both APIs and serverless functions in Go, eliminating the need to switch contexts between Go and previously supported languages. This is a game changer, as it enables type consistency across APIs and functions, streamlines your deployment process, and significantly enhances the developer experience.

The project repository can be found here.

Before the Go SDK

Prior to the Go SDK, if you want to build API with Go and Appwrite, you’ll first create a service that talks to Appwrite API:

1// get details from environment variable 2var projectId = GetEnvVariable("PROJECT_ID") 3var databaseId = GetEnvVariable("DATABASE_ID") 4var collectionId = GetEnvVariable("COLLECTION_ID") 5var apiKey = GetEnvVariable("PROJECT_ID") 6 7func (app *Config) createProject(newProject *data.ProjectRequest) (*data.ProjectResponse, error) { 8 url := fmt.Sprintf("https://cloud.appwrite.io/v1/databases/%s/collections/%s/documents", databaseId, collectionId) 9 10 createdProject := data.ProjectResponse{} 11 12 jsonData := data.JsonAPIBody{ 13 DocumentId: "unique()", 14 Data: newProject, 15 } 16 postBody, _ := json.Marshal(jsonData) 17 bodyData := bytes.NewBuffer(postBody) 18 19 // making the request 20 client := &http.Client{} 21 req, _ := http.NewRequest("POST", url, bodyData) 22 req.Header.Add("Content-Type", "application/json") 23 req.Header.Add("X-Appwrite-Key", apiKey) 24 req.Header.Add("X-Appwrite-Project", projectId) 25 26 resp, err := client.Do(req) 27 if err != nil { 28 return nil, err 29 } 30 31 body, err := ioutil.ReadAll(resp.Body) 32 if err != nil { 33 return nil, err 34 } 35 36 err = json.Unmarshal([]byte(body), &createdProject) 37 if err != nil { 38 return nil, err 39 } 40 return &createdProject, nil 41}

Then use the service to create a handler that exposes the API.

1// import dependencies 2 3const appTimeout = time.Second * 10 4 5func (app *Config) createdProjectHandler() gin.HandlerFunc { 6 return func(ctx *gin.Context) { 7 _, cancel := context.WithTimeout(context.Background(), appTimeout) 8 var payload data.ProjectRequest 9 defer cancel() 10 11 app.validateJsonBody(ctx, &payload) 12 13 newProject := data.ProjectRequest{ 14 Name: payload.Name, 15 Description: payload.Description, 16 } 17 18 data, err := app.createProject(&newProject) // using the service 19 if err != nil { 20 app.errorJSON(ctx, err) 21 return 22 } 23 24 app.writeJSON(ctx, http.StatusCreated, data) 25 } 26}

As seen above, you have to manually call the APIs using an HTTP client, configure headers to include the API key and project ID, switch URLs (when building other endpoints), handle requests, convert responses to JSON, and manage many other overhead tasks.

Now, imagine doing this for multiple requests across different endpoints. It quickly becomes a tedious process, with repetitive code and constant refactoring whenever you need to update the project.

But with Appwrite’s new Go SDK, you can build APIs seamlessly with minimal effort. Let’s take a closer look.

Getting started

To get started, navigate to the desired directory and run the command below:

1mkdir go-appwriteSDK && cd go-appwriteSDK

Next, initialize a Go module to manage project dependencies by running the command below:

1go mod init go-appwriteSDK

Finally, proceed to install the required dependencies with:

1go get github.com/appwrite/sdk-for-go github.com/gin-gonic/gin github.com/go-playground/validator/v10 github.com/joho/godotenv

github.com/appwrite/sdk-for-go is Appwrite Go SDK.

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 variables.

Structuring the application To do this, create an api, cmd, and data folder in your 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

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

To do this, first, navigate to the Database tab, click the Create database button, input project as the name, and Create.

Create database

Secondly, create a collection for storing the projects by clicking the Create collection button, input project_collection as the name, and then click Create.

Create collection

Lastly, you need to create attributes to represent the database fields. To do this, navigate to the Attributes tab and create attributes for each of the values shown below:

Attribute keyAttribute typeSizeRequired
nameString250YES
descriptionString5000YES

Create attribute

After creating the attributes, you should see them as shown below:

List of attributes

Create an API key

To securely connect to Appwrite, you need to create an API key. To do this, 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

Building the APIs fast with the Appwrite Go SDK

With the project fully set up on Appwrite, you can now use the database with the SDK to build the APIs without any other technical overhead.

Set up Environment Variable

To set up the required environment variables, you 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> 5API_URL=https://cloud.appwrite.io/v1

You can get the required key and IDs from your Appwrite console.

Create the API models and helper function

To represent the application data, 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 Data *ProjectRequest `json:"data,omitempty"` 21}

The snippet above creates a Project, ProjectRequest, ProjectResponse, and JsonAPIBody struct with the required properties to describe requests and response types.

Finally, create a helpers.go file with a reusable function to load environment variables and return JSON responses inside the api folder.

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 29 //validate the request body 30 if err := c.BindJSON(&data); err != nil { 31 return err 32 } 33 34 //validate with the validator library 35 if err := validate.Struct(&data); err != nil { 36 return err 37 } 38 return nil 39} 40 41func (app *Config) writeJSON(c *gin.Context, status int, data any) { 42 c.JSON(status, jsonResponse{Status: status, Message: "success", Data: data}) 43} 44 45func (app *Config) errorJSON(c *gin.Context, err error, status ...int) { 46 statusCode := http.StatusBadRequest 47 if len(status) > 0 { 48 statusCode = status[0] 49 } 50 c.JSON(statusCode, jsonResponse{Status: statusCode, Message: err.Error()}) 51}

Create the API route entry

Create a route.go file for configuring the API routes inside the api folder 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 handlers

To create the handlers, first, create a handler.go file inside the api folder and add the snippet below:

1package api 2 3import ( 4 "context" 5 "go-appwriteSDK/data" 6 "net/http" 7 "time" 8 "github.com/appwrite/sdk-for-go/appwrite" 9 "github.com/appwrite/sdk-for-go/id" 10 "github.com/gin-gonic/gin" 11) 12 13var projectId = GetEnvVariable("PROJECT_ID") 14var databaseId = GetEnvVariable("DATABASE_ID") 15var collectionId = GetEnvVariable("COLLECTION_ID") 16var apiKey = GetEnvVariable("API_KEY") 17var endpoint = GetEnvVariable("API_URL") 18 19var client = appwrite.NewClient( 20 appwrite.WithEndpoint(endpoint), 21 appwrite.WithProject(projectId), 22 appwrite.WithKey(apiKey), 23) 24 25const appTimeout = time.Second * 10

The snippet above imports the required dependencies, uses the GetEnvVariable to load the required enrionment variables, configures Appwrite with the required credentials, and creates a 10 seconds timeout for requests.

Finally, use the Appwrite instance and the timeout to create the handlers.

1func (app *Config) createProjectHandler() gin.HandlerFunc { 2 return func(ctx *gin.Context) { 3 _, cancel := context.WithTimeout(context.Background(), appTimeout) 4 databases := appwrite.NewDatabases(client) 5 var payload data.ProjectRequest 6 defer cancel() 7 8 app.validateJsonBody(ctx, &payload) 9 10 doc, err := databases.CreateDocument(databaseId, collectionId, id.Unique(), payload) 11 if err != nil { 12 app.errorJSON(ctx, err) 13 return 14 } 15 app.writeJSON(ctx, http.StatusCreated, doc) 16 } 17} 18 19func (app *Config) getProjectHandler() gin.HandlerFunc { 20 return func(ctx *gin.Context) { 21 _, cancel := context.WithTimeout(context.Background(), appTimeout) 22 databases := appwrite.NewDatabases(client) 23 docId := ctx.Param("projectId") 24 defer cancel() 25 26 response, err := databases.GetDocument(databaseId, collectionId, docId) 27 28 if err != nil { 29 app.errorJSON(ctx, err) 30 return 31 } 32 var document data.Project 33 err = response.Decode(&document) 34 if err != nil { 35 app.errorJSON(ctx, err) 36 return 37 } 38 39 app.writeJSON(ctx, http.StatusOK, document) 40 } 41} 42 43func (app *Config) updateProjectHandler() gin.HandlerFunc { 44 return func(ctx *gin.Context) { 45 _, cancel := context.WithTimeout(context.Background(), appTimeout) 46 databases := appwrite.NewDatabases(client) 47 var payload data.ProjectRequest 48 docId := ctx.Param("projectId") 49 defer cancel() 50 51 app.validateJsonBody(ctx, &payload) 52 updates := data.ProjectRequest{ 53 Name: payload.Name, 54 Description: payload.Description, 55 } 56 57 response, err := databases.UpdateDocument(databaseId, collectionId, docId, databases.WithUpdateDocumentData(updates)) 58 if err != nil { 59 app.errorJSON(ctx, err) 60 return 61 } 62 63 var document data.Project 64 err = response.Decode(&document) 65 if err != nil { 66 app.errorJSON(ctx, err) 67 return 68 } 69 70 app.writeJSON(ctx, http.StatusOK, document) 71 } 72} 73 74func (app *Config) deleteProjectHandler() gin.HandlerFunc { 75 return func(ctx *gin.Context) { 76 _, cancel := context.WithTimeout(context.Background(), appTimeout) 77 databases := appwrite.NewDatabases(client) 78 docId := ctx.Param("projectId") 79 defer cancel() 80 81 response, err := databases.DeleteDocument(databaseId, collectionId, docId) 82 if err != nil { 83 app.errorJSON(ctx, err) 84 return 85 } 86 87 app.writeJSON(ctx, http.StatusOK, response) 88 } 89}

Update the API routes to use handlers

With that done, update the routes.go file with the handlers as shown 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 app.Router.POST("/project", app.createProjectHandler()) 11 app.Router.GET("/project/:projectId", app.getProjectHandler()) 12 app.Router.PUT("/project/:projectId", app.updateProjectHandler()) 13 app.Router.DELETE("/project/:projectId", app.deleteProjectHandler()) 14}

Putting it all together

With the APIs fully set up, you need to create the application entry point. To do this, create a main.go file inside the cmd folder and add the snippet below:

1package main 2 3import ( 4 "go-appwriteSDK/api" 5 "github.com/gin-gonic/gin" 6) 7 8func main() { 9 router := gin.Default() 10 //initialize config 11 app := api.Config{Router: router} 12 //routes 13 app.Routes() 14 router.Run(":8080") 15}

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

Conclusion

This post discusses how to quickly build REST APIs with the Appwrite Go SDK, eliminating the need to manually configure headers for API keys and project IDs, switch URLs for different endpoints, handle requests, and convert responses to JSON.

These resources might be helpful:

Related posts