Appwrite + Go: Build APIs without technical overhead

Demola Malomo - Aug 18 '23 - - Dev Community

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:

mkdir go-appwrite && cd go-appwrite
Enter fullscreen mode Exit fullscreen mode

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:

go mod init go-appwrite
Enter fullscreen mode Exit fullscreen mode

This command will create a go.mod file for tracking the project dependencies.

Finally, we proceed to install the required dependencies with:

go get github.com/gin-gonic/gin github.com/go-playground/validator/v10 github.com/joho/godotenv
Enter fullscreen mode Exit fullscreen mode

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 key Attribute type Size Required
name String 250 YES
description String 5000 YES

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:

API_KEY=<REPLACE WITH API KEY>
PROJECT_ID=<REPLACE WITH PROJECT ID>
DATABASE_ID=<REPLACE WITH DATABASE ID>
COLLECTION_ID=<REPLACE WITH COLLECTION ID>
Enter fullscreen mode Exit fullscreen mode

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:

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 {
    DocumentId string          `json:"documentId,omitempty"`
    Data       *ProjectRequest `json:"data,omitempty"`
}
Enter fullscreen mode Exit fullscreen mode

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:

package api

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

type Config struct {
    Router *gin.Engine
}

func (app *Config) Routes() {
    //routes will come here
}
Enter fullscreen mode Exit fullscreen mode

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:

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()})
}
Enter fullscreen mode Exit fullscreen mode

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.

package api
import (
    "bytes"
    "encoding/json"
    "fmt"
    "go-appwrite/data"
    "io/ioutil"
    "net/http"
)
//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
}
Enter fullscreen mode Exit fullscreen mode

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.

//import goes here

func (app *Config) createProject(newProject *data.ProjectRequest) (*data.ProjectResponse, error) {
//createProject code goes here
}

func (app *Config) getProject(documentId string) (*data.Project, error) {
    url := fmt.Sprintf("https://cloud.appwrite.io/v1/databases/%s/collections/%s/documents/%s", databaseId, collectionId, documentId)

    projectDetail := data.Project{}

    //making the request
    client := &http.Client{}
    req, _ := http.NewRequest("GET", url, nil)
    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), &projectDetail)
    if err != nil {
        return nil, err
    }
    return &projectDetail, nil
}
Enter fullscreen mode Exit fullscreen mode

Thirdly, we need to add a updateProject function that uses similar logic as the createProject function to update the details of a project.

//import goes here

func (app *Config) createProject(newProject *data.ProjectRequest) (*data.ProjectResponse, error) {
//createProject code goes here
}

func (app *Config) getProject(documentId string) (*data.Project, error) {
    //getProject code goes here
}

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

    updates := data.ProjectResponse{}

    jsonData := data.JsonAPIBody{
        Data: updatedProject,
    }
    postBody, _ := json.Marshal(jsonData)
    bodyData := bytes.NewBuffer(postBody)

    //making the request
    client := &http.Client{}
    req, _ := http.NewRequest("PATCH", 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), &updates)
    if err != nil {
        return nil, err
    }
    return &updates, nil
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we need to add a deleteProject function that uses similar logic as the createProject function to delete the details of a project.

//import goes here

func (app *Config) createProject(newProject *data.ProjectRequest) (*data.ProjectResponse, error) {
//createProject code goes here
}

func (app *Config) getProject(documentId string) (*data.Project, error) {
    //getProject code goes here
}

func (app *Config) updateProject(updatedProject *data.ProjectRequest, documentId string) (*data.ProjectResponse, error) {
    //updateProject code goes here
}

func (app *Config) deleteProject(documentId string) (string, error) {
    url := fmt.Sprintf("https://cloud.appwrite.io/v1/databases/%s/collections/%s/documents/%s", databaseId, collectionId, documentId)

    //making the request
    client := &http.Client{}
    req, _ := http.NewRequest("DELETE", url, nil)
    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 "", err
    }
    _, err = ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return documentId, nil
}
Enter fullscreen mode Exit fullscreen mode

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:

package api

import (
    "context"
    "fmt"
    "go-appwrite/data"
    "net/http"
    "time"
    "github.com/gin-gonic/gin"
)

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)

        if err != nil {
            app.errorJSON(ctx, err)
            return
        }

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

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

        data, err := app.getProject(projectId)

        if err != nil {
            app.errorJSON(ctx, err)
            return
        }

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

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

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

        data, err := app.updateProject(&newProject, projectId)
        if err != nil {
            app.errorJSON(ctx, err)
            return
        }

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

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

        data, err := app.deleteProject(projectId)
        if err != nil {
            app.errorJSON(ctx, err)
            return
        }

        app.writeJSON(ctx, http.StatusAccepted, fmt.Sprintf("Project with ID: %s deleted successfully!!", data))
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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

type Config struct {
    Router *gin.Engine
}

func (app *Config) Routes() {
    app.Router.POST("/project", app.createdProjectHandler())
    app.Router.GET("/project/:projectId", app.getProjectHandler())
    app.Router.PATCH("/project/:projectId", app.updateProjectHandler())
    app.Router.DELETE("/project/:projectId", app.deleteProjectHandler())
}
Enter fullscreen mode Exit fullscreen mode

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:

package main

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

func main() {
    router := gin.Default()

    //initialize config
    app := api.Config{Router: router}

    //routes
    app.Routes()

    router.Run(":8080")
}
Enter fullscreen mode Exit fullscreen mode

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:

go run cmd/main.go
Enter fullscreen mode Exit fullscreen mode

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:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .