Xata + Go: A getting started guide.

malomz

Demola Malomo

Posted on February 5, 2024

Xata + Go: A getting started guide.

Xata is a serverless data platform for building modern and robust applications. Built on top of PostgreSQL, Xata provides a unified REST API for efficient data management. Setting itself apart from other data platforms, Xata introduces unique functionalities that significantly streamline the developer workflow. Here are some key benefits of integrating Xata into any application:

  • Robust file management: Xata provides APIs and SDKs to manage and securely upload images, documents, and more, directly to a database record.
  • Multiple environments support and workflow: With Xata, creating isolated production environments for testing, staging, or feature releases is seamless.
  • Fast search support: Xata automatically indexes uploaded data, facilitating fast and efficient data searches across tables and branches.
  • AI support: Xata offers vector embedding and AI solutions that empower the development of intelligent applications.

To experience the capabilities of Xata, we will build a project management API in Go. This API will offer features for creating, reading, updating, and deleting (CRUD) projects. The project repository can be found here.

Prerequisites

To follow along with this tutorial, the following are needed:

  • Basic understanding of Go
  • Xata account. Signup is free
  • Postman or any API testing application of your choice

Getting started

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

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

This command creates a Go project called go-xata 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-xata
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 the 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.

Setup the database on Xata

To get started, log into the Xata workspace and create a project database. Inside the project database, create a Project table and add columns as shown below:

Column type Column name
String name
Text description
String status

Create database
Add table

Add field
Add field

Inside a table, Xata automatically adds an id, xata.createdAt, xata.updatedAt, and xata.version columns that we can also leverage to perform advanced data operations.

Created column

Get the Database URL and set up the API Key

To securely connect to the database, Xata provides a unique and secure URL for accessing it. To get the database URL, click the Get code snippet button and copy the URL. Then click the API Key link, add a new key, save and copy the API key.



Setup environment variable

Next, we must add our database URL and API key as an environment variable. To do this, create .env file in the root directory and add the copied URL and API key.

XATA_DATABASE_URL= <REPLACE WITH THE COPIED DATABASE URL>
XATA_API_KEY=<REPLACE WITH THE COPIED API KEY>
Enter fullscreen mode Exit fullscreen mode

Building the project management API with Go and Xata

We can start building our API with our database fully set up on Xata and a secure URL created to access it.

Create the API models

To represent the application data, 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"`
    Status      string `json:"status,omitempty"`
}

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

type ProjectResponse struct {
    Id      string `json:"id,omitempty"`
}
Enter fullscreen mode Exit fullscreen mode

The snippet above creates a Project, ProjectRequest, and ProjectResponse 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

Next, we will create helper functions for our application to construct the API. 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 using the validator library
  • 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 can now use them to create our application logic. To do this, we need to create a xata_service.go file and update it by doing the following:

First, we need to import the required dependencies and create helper functions:

package api

import (
    "bytes"
    "encoding/json"
    "fmt"
    "go-xata/data"
    "io/ioutil"
    "net/http"
)

var xataAPIKey = GetEnvVariable("XATA_API_KEY")
var baseURL = GetEnvVariable("XATA_DATABASE_URL")

func createRequest(method, url string, bodyData *bytes.Buffer) (*http.Request, error) {
    var req *http.Request
    var err error

    if method == "GET" || method == "DELETE" {
        req, err = http.NewRequest(method, url, nil)
    } else {
        req, err = http.NewRequest(method, url, bodyData)
    }

    if err != nil {
        return nil, err
    }

    req.Header.Add("Content-Type", "application/json")
    req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", xataAPIKey))
    return req, nil
}

func makeRequest(req *http.Request, target interface{}) error {
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if target != nil {
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            return err
        }
        err = json.Unmarshal(body, target)
        if err != nil {
            return err
        }
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates required environment variables
  • Creates a createRequest function creates HTTP request with the required headers
  • Creates a makeRequest function that handles sending the request and parsing the response

Lastly, we need to add methods that use the helper methods to perform CRUD operations.

//imports goes here

var xataAPIKey = GetEnvVariable("XATA_API_KEY")
var baseURL = GetEnvVariable("XATA_DATABASE_URL")

func createRequest(method, url string, bodyData *bytes.Buffer) (*http.Request, error) {
    //createRequest code goes here
}

func makeRequest(req *http.Request, target interface{}) error {
    //makeRequest code goes here
}

func (app *Config) createProjectService(newProject *data.ProjectRequest) (*data.ProjectResponse, error) {
    createProject := data.ProjectResponse{}
    jsonData := data.Project{
        Name:        newProject.Name,
        Description: newProject.Description,
        Status:      newProject.Status,
    }

    postBody, _ := json.Marshal(jsonData)
    bodyData := bytes.NewBuffer(postBody)

    fullURL := fmt.Sprintf("%s:main/tables/Project/data", baseURL)
    req, err := createRequest("POST", fullURL, bodyData)
    if err != nil {
        return nil, err
    }

    err = makeRequest(req, &createProject)
    if err != nil {
        return nil, err
    }

    return &createProject, nil
}

func (app *Config) getProjectService(projectId string) (*data.Project, error) {
    projectDetails := data.Project{}
    fullURL := fmt.Sprintf("%s:main/tables/Project/data/%s", baseURL, projectId)
    req, err := createRequest("GET", fullURL, nil)
    if err != nil {
        return nil, err
    }

    err = makeRequest(req, &projectDetails)
    if err != nil {
        return nil, err
    }

    return &projectDetails, nil
}

func (app *Config) updateProjectService(updatedProject *data.ProjectRequest, projectId string) (*data.ProjectResponse, error) {
    updateProject := data.ProjectResponse{}
    jsonData := data.Project{
        Name:        updatedProject.Name,
        Description: updatedProject.Description,
        Status:      updatedProject.Status,
    }

    postBody, _ := json.Marshal(jsonData)
    bodyData := bytes.NewBuffer(postBody)

    fullURL := fmt.Sprintf("%s:main/tables/Project/data/%s", baseURL, projectId)
    req, err := createRequest("PUT", fullURL, bodyData)
    if err != nil {
        return nil, err
    }

    err = makeRequest(req, &updateProject)
    if err != nil {
        return nil, err
    }

    return &updateProject, nil
}

func (app *Config) deleteProjectService(projectId string) (string, error) {
    fullURL := fmt.Sprintf("%s:main/tables/Project/data/%s", baseURL, projectId)
    req, err := createRequest("DELETE", fullURL, nil)
    if err != nil {
        return "", err
    }

    err = makeRequest(req, nil)
    if err != nil {
        return "", err
    }
    return projectId, nil
}
Enter fullscreen mode Exit fullscreen mode

The snippet above creates a createProjectService, getProjectService, updateProjectService, and deleteProjectService methods for performing CRUD operations.

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-xata/data"
    "net/http"
    "time"
    "github.com/gin-gonic/gin"
)

const appTimeout = time.Second * 10

func (app *Config) createProjectHandler() 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,
            Status:      payload.Status,
        }

        data, err := app.createProjectService(&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.getProjectService(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,
            Status:      payload.Status,
        }

        data, err := app.updateProjectService(&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.deleteProjectService(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 return 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.createProjectHandler())
    app.Router.GET("/project/:projectId", app.getProjectHandler())
    app.Router.PUT("/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 must 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-xata/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 project
Get a project
Update a project

Delete a project

We can also confirm the project management data by checking the table on Xata.

Data on Xata

Conclusion

This post discusses what Xata is and provides a detailed step-by-step guide to using it to build a project management API in Go. In addition to the functionalities explored earlier, Xata also includes well-tailored features that developers can harness to build applications ranging from small to large.

These resources may also be helpful:

💖 💪 🙅 🚩
malomz
Demola Malomo

Posted on February 5, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related