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
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
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
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.
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 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.
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.
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 |
After creating the attributes, we see them as shown below:
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.
Next, input api_go
as the name, click the Next button, select Database as the required scope, and Create.
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>
We can get the required API key and IDs from our Appwrite console as shown below:
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"`
}
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
}
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 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()})
}
The snippet above does the following:
- Imports the required dependencies
- Creates a
jsonResponse
struct to describe the API response - Creates a
GetEnvVariable
function that usesgodotenv
package to load and get environment variable - Creates a
validateBody
function that takes in theConfig
struct as a pointer and returns anerror
. Inside the function, we validate that requestdata
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 theConfig
struct as a pointer and uses thejsonResponse
struct to construct API response when there’s no error - Creates a
errorJSON
function that takes in theConfig
struct as a pointer and uses thejsonResponse
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
}
The snippet above does the following:
- Imports the required dependencies
- Creates required environment variables
- Creates a
createProject
function that takes in theConfig
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
}
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
}
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
}
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))
}
}
The snippet above does the following:
- Imports the required dependencies
- Creates a
createdProjectHandler
,getProjectHandler
,updateProjectHandler
, anddeleteProjectHandler
functions that returns a Gin-gonic handler and takes in theConfig
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())
}
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")
}
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:
go run cmd/main.go
We can also confirm the project management data by checking the collection on Appwrite.
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: