Xata + Go: A getting started guide.
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.
Demola Malomo
Feb 05 2024
8 min read
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:
1mkdir go-xata && cd go-xata
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:
1go mod init go-xata
This command will create a go.mod
file for tracking the project dependencies.
Finally, we proceed to install the required dependencies with:
1go 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 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 |
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.
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.
1XATA_DATABASE_URL= <REPLACE WITH THE COPIED DATABASE URL> 2XATA_API_KEY=<REPLACE WITH THE COPIED API KEY>
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:
1package data 2 3type Project struct { 4 Id string `json:"id,omitempty"` 5 Name string `json:"name,omitempty"` 6 Description string `json:"description,omitempty"` 7 Status string `json:"status,omitempty"` 8} 9 10type ProjectRequest struct { 11 Name string `json:"name,omitempty"` 12 Description string `json:"description,omitempty"` 13 Status string `json:"status,omitempty"` 14} 15 16type ProjectResponse struct { 17 Id string `json:"id,omitempty"` 18}
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:
1package api 2 3import "github.com/gin-gonic/gin" 4 5type Config struct { 6 Router *gin.Engine 7} 8 9func (app *Config) Routes() { 10 //routes will come here 11}
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
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:
1package api 2 3import ( 4 "log" 5 "net/http" 6 "os" 7 "github.com/gin-gonic/gin" 8 "github.com/go-playground/validator/v10" 9 "github.com/joho/godotenv" 10) 11 12type jsonResponse struct { 13 Status int `json:"status"` 14 Message string `json:"message"` 15 Data any `json:"data"` 16} 17 18func GetEnvVariable(key string) string { 19 err := godotenv.Load() 20 if err != nil { 21 log.Fatal("Error loading .env file") 22 } 23 return os.Getenv(key) 24} 25 26func (app *Config) validateJsonBody(c *gin.Context, data any) error { 27 var validate = validator.New() 28 29 //validate the request body 30 if err := c.BindJSON(&data); err != nil { 31 return err 32 } 33 34 //validate with the validator library 35 if err := validate.Struct(&data); err != nil { 36 return err 37 } 38 return nil 39} 40 41func (app *Config) writeJSON(c *gin.Context, status int, data any) { 42 c.JSON(status, jsonResponse{Status: status, Message: "success", Data: data}) 43} 44 45func (app *Config) errorJSON(c *gin.Context, err error, status ...int) { 46 statusCode := http.StatusBadRequest 47 48 if len(status) > 0 { 49 statusCode = status[0] 50 } 51 c.JSON(statusCode, jsonResponse{Status: statusCode, Message: err.Error()}) 52}
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 using the validator library - 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 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:
1package api 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "go-xata/data" 8 "io/ioutil" 9 "net/http" 10) 11 12var xataAPIKey = GetEnvVariable("XATA_API_KEY") 13var baseURL = GetEnvVariable("XATA_DATABASE_URL") 14 15func createRequest(method, url string, bodyData *bytes.Buffer) (*http.Request, error) { 16 var req *http.Request 17 var err error 18 19 if method == "GET" || method == "DELETE" { 20 req, err = http.NewRequest(method, url, nil) 21 } else { 22 req, err = http.NewRequest(method, url, bodyData) 23 } 24 25 if err != nil { 26 return nil, err 27 } 28 29 req.Header.Add("Content-Type", "application/json") 30 req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", xataAPIKey)) 31 return req, nil 32} 33 34func makeRequest(req *http.Request, target interface{}) error { 35 client := &http.Client{} 36 resp, err := client.Do(req) 37 if err != nil { 38 return err 39 } 40 defer resp.Body.Close() 41 42 if target != nil { 43 body, err := ioutil.ReadAll(resp.Body) 44 if err != nil { 45 return err 46 } 47 err = json.Unmarshal(body, target) 48 if err != nil { 49 return err 50 } 51 } 52 return nil 53}
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.
1//imports goes here 2 3var xataAPIKey = GetEnvVariable("XATA_API_KEY") 4var baseURL = GetEnvVariable("XATA_DATABASE_URL") 5 6func createRequest(method, url string, bodyData *bytes.Buffer) (*http.Request, error) { 7 //createRequest code goes here 8} 9 10func makeRequest(req *http.Request, target interface{}) error { 11 //makeRequest code goes here 12} 13 14func (app *Config) createProjectService(newProject *data.ProjectRequest) (*data.ProjectResponse, error) { 15 createProject := data.ProjectResponse{} 16 jsonData := data.Project{ 17 Name: newProject.Name, 18 Description: newProject.Description, 19 Status: newProject.Status, 20 } 21 22 postBody, _ := json.Marshal(jsonData) 23 bodyData := bytes.NewBuffer(postBody) 24 25 fullURL := fmt.Sprintf("%s:main/tables/Project/data", baseURL) 26 req, err := createRequest("POST", fullURL, bodyData) 27 if err != nil { 28 return nil, err 29 } 30 31 err = makeRequest(req, &createProject) 32 if err != nil { 33 return nil, err 34 } 35 36 return &createProject, nil 37} 38 39func (app *Config) getProjectService(projectId string) (*data.Project, error) { 40 projectDetails := data.Project{} 41 fullURL := fmt.Sprintf("%s:main/tables/Project/data/%s", baseURL, projectId) 42 req, err := createRequest("GET", fullURL, nil) 43 if err != nil { 44 return nil, err 45 } 46 47 err = makeRequest(req, &projectDetails) 48 if err != nil { 49 return nil, err 50 } 51 52 return &projectDetails, nil 53} 54 55func (app *Config) updateProjectService(updatedProject *data.ProjectRequest, projectId string) (*data.ProjectResponse, error) { 56 updateProject := data.ProjectResponse{} 57 jsonData := data.Project{ 58 Name: updatedProject.Name, 59 Description: updatedProject.Description, 60 Status: updatedProject.Status, 61 } 62 63 postBody, _ := json.Marshal(jsonData) 64 bodyData := bytes.NewBuffer(postBody) 65 66 fullURL := fmt.Sprintf("%s:main/tables/Project/data/%s", baseURL, projectId) 67 req, err := createRequest("PUT", fullURL, bodyData) 68 if err != nil { 69 return nil, err 70 } 71 72 err = makeRequest(req, &updateProject) 73 if err != nil { 74 return nil, err 75 } 76 77 return &updateProject, nil 78} 79 80func (app *Config) deleteProjectService(projectId string) (string, error) { 81 fullURL := fmt.Sprintf("%s:main/tables/Project/data/%s", baseURL, projectId) 82 req, err := createRequest("DELETE", fullURL, nil) 83 if err != nil { 84 return "", err 85 } 86 87 err = makeRequest(req, nil) 88 if err != nil { 89 return "", err 90 } 91 return projectId, nil 92}
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:
1package api 2 3import ( 4 "context" 5 "fmt" 6 "go-xata/data" 7 "net/http" 8 "time" 9 "github.com/gin-gonic/gin" 10) 11 12const appTimeout = time.Second * 10 13 14func (app *Config) createProjectHandler() gin.HandlerFunc { 15 return func(ctx *gin.Context) { 16 _, cancel := context.WithTimeout(context.Background(), appTimeout) 17 var payload data.ProjectRequest 18 defer cancel() 19 20 app.validateJsonBody(ctx, &payload) 21 22 newProject := data.ProjectRequest{ 23 Name: payload.Name, 24 Description: payload.Description, 25 Status: payload.Status, 26 } 27 28 data, err := app.createProjectService(&newProject) 29 if err != nil { 30 app.errorJSON(ctx, err) 31 return 32 } 33 34 app.writeJSON(ctx, http.StatusCreated, data) 35 } 36} 37 38func (app *Config) getProjectHandler() gin.HandlerFunc { 39 return func(ctx *gin.Context) { 40 _, cancel := context.WithTimeout(context.Background(), appTimeout) 41 projectId := ctx.Param("projectId") 42 defer cancel() 43 44 data, err := app.getProjectService(projectId) 45 if err != nil { 46 app.errorJSON(ctx, err) 47 return 48 } 49 50 app.writeJSON(ctx, http.StatusOK, data) 51 } 52} 53 54func (app *Config) updateProjectHandler() gin.HandlerFunc { 55 return func(ctx *gin.Context) { 56 _, cancel := context.WithTimeout(context.Background(), appTimeout) 57 projectId := ctx.Param("projectId") 58 var payload data.ProjectRequest 59 defer cancel() 60 61 app.validateJsonBody(ctx, &payload) 62 63 newProject := data.ProjectRequest{ 64 Name: payload.Name, 65 Description: payload.Description, 66 Status: payload.Status, 67 } 68 69 data, err := app.updateProjectService(&newProject, projectId) 70 if err != nil { 71 app.errorJSON(ctx, err) 72 return 73 } 74 75 app.writeJSON(ctx, http.StatusOK, data) 76 } 77} 78 79func (app *Config) deleteProjectHandler() gin.HandlerFunc { 80 return func(ctx *gin.Context) { 81 _, cancel := context.WithTimeout(context.Background(), appTimeout) 82 projectId := ctx.Param("projectId") 83 defer cancel() 84 85 data, err := app.deleteProjectService(projectId) 86 if err != nil { 87 app.errorJSON(ctx, err) 88 return 89 } 90 91 app.writeJSON(ctx, http.StatusAccepted, fmt.Sprintf("Project with ID: %s deleted successfully!!", data)) 92 } 93}
The snippet above does the following:
- Imports the required dependencies
- Creates a
createdProjectHandler
,getProjectHandler
,updateProjectHandler
, anddeleteProjectHandler
functions that return 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:
1package api 2 3import "github.com/gin-gonic/gin" 4 5type Config struct { 6 Router *gin.Engine 7} 8 9func (app *Config) Routes() { 10 app.Router.POST("/project", app.createProjectHandler()) 11 app.Router.GET("/project/:projectId", app.getProjectHandler()) 12 app.Router.PUT("/project/:projectId", app.updateProjectHandler()) 13 app.Router.DELETE("/project/:projectId", app.deleteProjectHandler()) 14}
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:
1package main 2 3import ( 4 "go-xata/api" 5 "github.com/gin-gonic/gin" 6) 7 8func main() { 9 router := gin.Default() 10 11 //initialize config 12 app := api.Config{Router: router} 13 14 //routes 15 app.Routes() 16 17 router.Run(":8080") 18}
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:
1go run cmd/main.go
We can also confirm the project management data by checking the table 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: