Appwrite + Go: Build APIs without technical overhead
Tags: go, appwrite, api, baas
Published: 2023-08-18 08:21:34

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)](https://www.cloudflare.com/en-gb/learning/serverless/glossary/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](https://github.com/Mr-Malomz/go-appwrite).

## 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](https://cloud.appwrite.io/register)

## Getting started

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

```bash
mkdir 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:

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

```bash
go 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](https://paper-attachments.dropboxusercontent.com/s_38120870E4DF4BFB47C9B36A260630109062D7DB843FAE021331CAC8C17C9E14_1691874765421_Screenshot+2023-08-12+at+22.12.33.png)

`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](https://cloud.appwrite.io/console), click the **Create project** button, input `go_appwrite` as the name, and then **Create**.

![Create project](https://paper-attachments.dropboxusercontent.com/s_C9210607DAFF6D80615947ED6BE237716E98BADEF0416B60FACF081B1CAEFB80_1687518026974_Screenshot+2023-06-23+at+11.38.55.png)

**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](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691705236741_Screenshot+2023-08-10+at+23.06.00.png)

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](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691705351204_Screenshot+2023-08-04+at+01.14.37.png)

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](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691705420699_Screenshot+2023-08-04+at+01.15.49.png)
![create](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691705440090_Screenshot+2023-08-04+at+01.19.11.png)

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

![List of attributes](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691705645782_Screenshot+2023-08-04+at+01.21.03.png)

**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](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691709339925_Screenshot+2023-08-04+at+01.37.13.png)

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

![input  name create](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691709339959_Screenshot+2023-08-04+at+01.39.23.png)
![Set permission](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691709613147_Screenshot+2023-08-04+at+01.40.32.png)

## 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:

```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>
```

We can get the required API key and IDs from our Appwrite console as shown below:

![API key](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691711034772_Screenshot+2023-08-04+at+01.45.30.png)

![Project ID](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691711069809_Screenshot+2023-08-04+at+01.24.17.png)

![Database and Collection ID](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691711142955_Screenshot+2023-08-11+at+00.44.58.png)

**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:

```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 {
    DocumentId string          `json:"documentId,omitempty"`
    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.

**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:

```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 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:

```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()})
}
```

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.

```go
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
}
```

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.

```go
//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
}
```

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

```go
//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
}
```

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

```go
//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
}
```

**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:

```go
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))
    }
}
```

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:

```go
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())
}
```

**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:

```go
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")
}
```

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](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691715978196_Screenshot+2023-08-05+at+17.11.09.png)
![update](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691715978260_Screenshot+2023-08-06+at+00.16.12.png)

![Get details](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691715978225_Screenshot+2023-08-05+at+22.31.35.png)
![Delete](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691715978285_Screenshot+2023-08-06+at+00.30.24.png)

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

![Detail](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691716190575_Screenshot+2023-08-11+at+02.08.04.png)

# 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:

-   [Appwrite official documentation](https://appwrite.io/docs?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)
-   [Appwrite database API](https://appwrite.io/docs/client/databases?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)
-   [Gin-gonic web framework](https://gin-gonic.com/?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)
-   [Go HTTP Library](https://pkg.go.dev/net/http?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)
-   [Time-saving with Appwrite](https://dev.to/hackmamba/save-time-earn-more-the-benefits-of-using-appwrite-cloud-for-freelance-developers-1m89)