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.
Demola Malomo
Dec 10 2024
6 min read
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 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.
Secondly, create a collection for storing the projects by clicking the Create collection button, input project_collection
as the name, and then click Create.
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 key | Attribute type | Size | Required |
---|---|---|---|
name | String | 250 | YES |
description | String | 5000 | YES |
After creating the attributes, you should see them as shown below:
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.
Next, input api_go
as the name, click the Next button, select Database as the required scope, and Create.
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 aRouter
property to configure the application methods. - Creates a
Routes
function that takes in theConfig
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 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
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: