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:

go
// get details from environment variable
var projectId = GetEnvVariable("PROJECT_ID")
var databaseId = GetEnvVariable("DATABASE_ID")
var collectionId = GetEnvVariable("COLLECTION_ID")
var apiKey = GetEnvVariable("PROJECT_ID")

func (app *Config) createProject(newProject *data.ProjectRequest) (*data.ProjectResponse, error) {
    url := fmt.Sprintf("https://cloud.appwrite.io/v1/databases/%s/collections/%s/documents", databaseId, collectionId)

    createdProject := data.ProjectResponse{}

    jsonData := data.JsonAPIBody{
        DocumentId: "unique()",
        Data:       newProject,
    }
    postBody, _ := json.Marshal(jsonData)
    bodyData := bytes.NewBuffer(postBody)

    // making the request
    client := &http.Client{}
    req, _ := http.NewRequest("POST", url, bodyData)
    req.Header.Add("Content-Type", "application/json")
    req.Header.Add("X-Appwrite-Key", apiKey)
    req.Header.Add("X-Appwrite-Project", projectId)

    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    err = json.Unmarshal([]byte(body), &createdProject)
    if err != nil {
        return nil, err
    }
    return &createdProject, nil
}

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

go
// import dependencies

const appTimeout = time.Second * 10

func (app *Config) createdProjectHandler() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        _, cancel := context.WithTimeout(context.Background(), appTimeout)
        var payload data.ProjectRequest
        defer cancel()

        app.validateJsonBody(ctx, &payload)

        newProject := data.ProjectRequest{
            Name:        payload.Name,
            Description: payload.Description,
        }

        data, err := app.createProject(&newProject) // using the service
        if err != nil {
            app.errorJSON(ctx, err)
            return
        }

        app.writeJSON(ctx, http.StatusCreated, data)
    }
}

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:

bash
mkdir go-appwriteSDK && cd go-appwriteSDK

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

bash
go mod init go-appwriteSDK

Finally, proceed to install the required dependencies with:

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

bash
API_KEY=<REPLACE WITH API KEY>
PROJECT_ID=<REPLACE WITH PROJECT ID>
DATABASE_ID=<REPLACE WITH DATABASE ID>
COLLECTION_ID=<REPLACE WITH COLLECTION ID>
API_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:

go
package data

type Project struct {
    Id          string `json:"$id,omitempty"`
    Name        string `json:"name,omitempty"`
    Description string `json:"description,omitempty"`
}

type ProjectRequest struct {
    Name        string `json:"name,omitempty"`
    Description string `json:"description,omitempty"`
}

type ProjectResponse struct {
    Id           string `json:"$id,omitempty"`
    CollectionId string `json:"$collectionId,omitempty"`
}

type JsonAPIBody struct {
    Data       *ProjectRequest `json:"data,omitempty"`
}

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.

go
package api

import (
    "log"
    "net/http"
    "os"
    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
    "github.com/joho/godotenv"
)

type jsonResponse struct {
    Status  int    `json:"status"`
    Message string `json:"message"`
    Data    any    `json:"data"`
}

func GetEnvVariable(key string) string {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }
    return os.Getenv(key)
}

func (app *Config) validateJsonBody(c *gin.Context, data any) error {
    var validate = validator.New()

    //validate the request body
    if err := c.BindJSON(&data); err != nil {
        return err
    }

    //validate with the validator library
    if err := validate.Struct(&data); err != nil {
        return err
    }
    return nil
}

func (app *Config) writeJSON(c *gin.Context, status int, data any) {
    c.JSON(status, jsonResponse{Status: status, Message: "success", Data: data})
}

func (app *Config) errorJSON(c *gin.Context, err error, status ...int) {
    statusCode := http.StatusBadRequest
    if len(status) > 0 {
        statusCode = status[0]
    }
    c.JSON(statusCode, jsonResponse{Status: statusCode, Message: err.Error()})
}

Create the API route entry

Create a route.go file for configuring the API routes inside the api folder and add the snippet below:

go
package api

import "github.com/gin-gonic/gin"

type Config struct {
    Router *gin.Engine
}

func (app *Config) Routes() {
    //routes will come here
}

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:

go
package api

import (
    "context"
    "go-appwriteSDK/data"
    "net/http"
    "time"
    "github.com/appwrite/sdk-for-go/appwrite"
    "github.com/appwrite/sdk-for-go/id"
    "github.com/gin-gonic/gin"
)

var projectId = GetEnvVariable("PROJECT_ID")
var databaseId = GetEnvVariable("DATABASE_ID")
var collectionId = GetEnvVariable("COLLECTION_ID")
var apiKey = GetEnvVariable("API_KEY")
var endpoint = GetEnvVariable("API_URL")

var client = appwrite.NewClient(
    appwrite.WithEndpoint(endpoint),
    appwrite.WithProject(projectId),
    appwrite.WithKey(apiKey),
)

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

go
func (app *Config) createProjectHandler() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        _, cancel := context.WithTimeout(context.Background(), appTimeout)
        databases := appwrite.NewDatabases(client)
        var payload data.ProjectRequest
        defer cancel()

        app.validateJsonBody(ctx, &payload)

        doc, err := databases.CreateDocument(databaseId, collectionId, id.Unique(), payload)
        if err != nil {
            app.errorJSON(ctx, err)
            return
        }
        app.writeJSON(ctx, http.StatusCreated, doc)
    }
}

func (app *Config) getProjectHandler() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        _, cancel := context.WithTimeout(context.Background(), appTimeout)
        databases := appwrite.NewDatabases(client)
        docId := ctx.Param("projectId")
        defer cancel()

        response, err := databases.GetDocument(databaseId, collectionId, docId)

        if err != nil {
            app.errorJSON(ctx, err)
            return
        }
        var document data.Project
        err = response.Decode(&document)
        if err != nil {
            app.errorJSON(ctx, err)
            return
        }

        app.writeJSON(ctx, http.StatusOK, document)
    }
}

func (app *Config) updateProjectHandler() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        _, cancel := context.WithTimeout(context.Background(), appTimeout)
        databases := appwrite.NewDatabases(client)
        var payload data.ProjectRequest
        docId := ctx.Param("projectId")
        defer cancel()

        app.validateJsonBody(ctx, &payload)
        updates := data.ProjectRequest{
            Name:        payload.Name,
            Description: payload.Description,
        }

        response, err := databases.UpdateDocument(databaseId, collectionId, docId, databases.WithUpdateDocumentData(updates))
        if err != nil {
            app.errorJSON(ctx, err)
            return
        }

        var document data.Project
        err = response.Decode(&document)
        if err != nil {
            app.errorJSON(ctx, err)
            return
        }

        app.writeJSON(ctx, http.StatusOK, document)
    }
}

func (app *Config) deleteProjectHandler() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        _, cancel := context.WithTimeout(context.Background(), appTimeout)
        databases := appwrite.NewDatabases(client)
        docId := ctx.Param("projectId")
        defer cancel()

        response, err := databases.DeleteDocument(databaseId, collectionId, docId)
        if err != nil {
            app.errorJSON(ctx, err)
            return
        }

        app.writeJSON(ctx, http.StatusOK, response)
    }
}

Update the API routes to use handlers

With that done, update the routes.go file with the handlers as shown below:

go
package api

import "github.com/gin-gonic/gin"

type Config struct {
    Router *gin.Engine
}

func (app *Config) Routes() {
    app.Router.POST("/project", app.createProjectHandler())
    app.Router.GET("/project/:projectId", app.getProjectHandler())
    app.Router.PUT("/project/:projectId", app.updateProjectHandler())
    app.Router.DELETE("/project/:projectId", app.deleteProjectHandler())
}

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:

go
package main

import (
    "go-appwriteSDK/api"
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    //initialize config
    app := api.Config{Router: router}
    //routes
    app.Routes()
    router.Run(":8080")
}

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:

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