[Part 2] Building and Deploying a REST API with Golang, Clean Architecture, Postgres, and render.com

Hien Luong
6 min readAug 13, 2023

--

Introduction

Welcome back to the second part of our tutorial! In previous part, we set up very basic application to connect with postgreSQL.

Here, we’ll create the core parts of our app: the repository, use case, and handler. These layers help us handle data, logic, and user requests.

Source code of part two is available on this branch.

Application design overview

  • Repository: This layer interacts directly with the database, handling data storage and retrieval.
  • Service: The service layer houses the core business logic, orchestrating how data flows between different components.
  • Handler: Responsible for managing incoming HTTP requests from users via their browsers, this layer plays a pivotal role in connecting the user interface to the application logic.

Now, let’s get down to work and start building the backbone of our application.

Defining Data Access and Business Logic

We’ll begin by setting up basic stuff. We’ll make files and define simple rules. Don’t worry, it’s easy!

Open a new file named todo.go inside the internal/domain folder. Here's what we'll put in it:

// internal/domain/todo.go
package domain

import "context"

type TodoID int

type Todo struct {
ID TodoID `db:"id" json:"id"`
Body string `db:"body" json:"body"`
Completed bool `db:"completed" json:"completed"`
}

type TodoRepository interface {
Select(ctx context.Context) ([]*Todo, error)
Insert(ctx context.Context, body string) (*Todo, error)
}

type TodoUsecase interface {
List(ctx context.Context) ([]*Todo, error)
Create(ctx context.Context, body string) (*Todo, error)
}
  • Todo struct: This defines how we store a todo. We describe how it appears in the database (db), and how it's presented as data when we interact with the app (json).
  • TodoRepository interface: We lay out two tasks here: getting todos and adding todos
  • TodoUsecase interface: This is how people see todos and create new ones.

Repository implementation

We’ll create a repository that interacts with the database and handles READ, CREATE operations. Here’s the code snippet:

// internal/repository/todo_repository.go
package repository

import (
"context"

"github.com/hienvl125/todo-api/internal/domain"
"github.com/jmoiron/sqlx"
)

type todoRepository struct {
db *sqlx.DB
}

func NewTodoRepository(db *sqlx.DB) domain.TodoRepository {
return &todoRepository{db: db}
}

func (r todoRepository) Select(ctx context.Context) ([]*domain.Todo, error) {
todos := []*domain.Todo{}
if err := r.db.Select(&todos, "SELECT * FROM todos ORDER BY id DESC"); err != nil {
return nil, err
}

return todos, nil
}

func (r todoRepository) Insert(ctx context.Context, body string) (*domain.Todo, error) {
lastInsertId := 0
if err := r.db.
QueryRow("INSERT INTO todos(body) VALUES($1) RETURNING id", body).
Scan(&lastInsertId); err != nil {
return nil, err
}

var todo domain.Todo
if err := r.db.Get(&todo, "SELECT * FROM todos WHERE id = $1", lastInsertId); err != nil {
return nil, err
}

return &todo, nil
}

Note: You might wonder why we use the “RETURNING id” clause in the INSERT statement. PostgreSQL doesn’t natively support returning IDs, so we manually retrieve and scan the ID into a variable to ensure we capture the ID of the newly inserted todo.

Moving on to the usecase layer.

Usecase Implementation

The usecase layer handles business logic and uses the repository. Here’s the code:

// internal/service/todo_service.go
package service

import (
"context"

"github.com/hienvl125/todo-api/internal/domain"
)

type todoService struct {
todoRepository domain.TodoRepository
}

func NewTodoService(todoRepository domain.TodoRepository) domain.TodoUsecase {
return &todoService{todoRepository: todoRepository}
}

func (s todoService) List(ctx context.Context) ([]*domain.Todo, error) {
todos, err := s.todoRepository.Select(ctx)
if err != nil {
return nil, err
}

return todos, nil

}

func (s todoService) Create(ctx context.Context, body string) (*domain.Todo, error) {
todo, err := s.todoRepository.Insert(ctx, body)
if err != nil {
return nil, err
}

return todo, nil
}

Lastly, the handler layer handles HTTP requests and uses the usecase layer.

Handler Implementation

// internal/handler/todo_handler.go
package handler

import (
"net/http"

"github.com/gin-gonic/gin"
"github.com/hienvl125/todo-api/internal/domain"
)

type TodoHandler struct {
todoService domain.TodoUsecase
}

func NewTodoHandler(todoService domain.TodoUsecase) *TodoHandler {
return &TodoHandler{todoService: todoService}
}

func (h TodoHandler) List(ctx *gin.Context) {
todos, err := h.todoService.List(ctx)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}

ctx.JSON(http.StatusOK, gin.H{
"todos": todos,
})
}

func (h TodoHandler) Create(ctx *gin.Context) {
var todoInput domain.Todo
if err := ctx.BindJSON(&todoInput); err != nil {
ctx.AbortWithError(http.StatusBadRequest, err)
return
}

if todoInput.Body == "" {
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"message": "body can't be blank",
})
return
}

todo, err := h.todoService.Create(ctx, todoInput.Body)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}

ctx.JSON(http.StatusOK, gin.H{
"todo": todo,
})
}

You might notice we use different methods like ctx.AbortWithError, ctx.AbortWithStatusJSON, and ctx.JSON. Here's why:

  • ctx.AbortWithError: This logs internal errors without exposing them outside.
  • ctx.AbortWithStatusJSON: Used for validation errors that are okay to show to users.
  • ctx.JSON: Responds with a JSON-format HTTP response.

Setup routes

Let’s craft a function to establish our handlers(routes) for our application. This function will play a pivotal role in defining the behavior of our API.

// internal/handler/handler.go
package handler

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

func SetupHandlers(
todoHandler *TodoHandler,
) *gin.Engine {
router := gin.Default()

todoRouterGroup := router.Group("/todos")
todoRouterGroup.GET("/", todoHandler.List)
todoRouterGroup.POST("/", todoHandler.Create)

return router
}

Connect everything

In part 1, we created the main.go file to establish and test our database connection. Now, let's take the next step and enhance our application by integrating the Gin framework to set up an HTTP server and test its functionality.

// cmd/api/main.go
package main
...

func main() {
...
todoRepository := repository.NewTodoRepository(db)
todoService := service.NewTodoService(todoRepository)
todoHandler := handler.NewTodoHandler(todoService)
ginServer := handler.SetupHandlers(todoHandler)
if err := ginServer.Run(conf.GinPort); err != nil {
log.Fatalln(err)
}
}

Let’s start the server and put our APIs to the test!

export $(cat local.env | xargs)
go run cmd/api/main.go

Creating a New Todo:

curl -X POST 'localhost:7070/todos' \
-H 'Content-Type: application/json' \
-d '{
"body": "Create a new todo"
}'

After running this command, you should receive a response like:

{"todo":{"id":18,"body":"Create a new todo","completed":false}}

Listing Todos:

curl --location 'localhost:7070/todos'

You’ll get a response similar to:

{"todos":[{"id":18,"body":"Create a new todo","completed":false}]}

Dependency injections with Wire

As we journey forward in crafting our RestAPI, let’s introduce you to the magic of google/wire for handling dependencies. This ingenious tool offers a simpler way compared to manual dependency setup.

we’ll take a few steps to neatly organize our code:

1. Separating configurations:

// internal/util/config/config.go
package config

import (
"fmt"

"github.com/kelseyhightower/envconfig"
)

type Config struct {
Dsn string `required:"true"`
GinPort string `required:"true" split_words:"true"`
}

func LoadConfig() (*Config, error) {
var c Config
err := envconfig.Process("", &c)
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}

return &c, nil
}

2. Separating database connection:

// internal/util/db/db.go
package db

import (
"github.com/hienvl125/todo-api/internal/util/config"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)

func NewPostgresDB(conf *config.Config) (*sqlx.DB, error) {
db, err := sqlx.Connect("postgres", conf.Dsn)
if err != nil {
return nil, err
}

return db, nil
}

3. Prepare the registry

// internal/registry/wire.go
// +build wireinject

package registry

import (
"github.com/gin-gonic/gin"
"github.com/google/wire"
"github.com/hienvl125/todo-api/internal/handler"
"github.com/hienvl125/todo-api/internal/repository"
"github.com/hienvl125/todo-api/internal/service"
"github.com/hienvl125/todo-api/internal/util/config"
"github.com/hienvl125/todo-api/internal/util/db"
)

func NewGinServer(conf *config.Config) (*gin.Engine, error) {
wire.Build(
handler.SetupHandlers,
handler.NewTodoHandler,
service.NewTodoService,
repository.NewTodoRepository,
db.NewPostgresDB,
)

return nil, nil
}

Note: +build wireinject directive to indicate files that are used for the code generation process.

4. Generating Dependencies:

With a simple command, generate dependency injections based on the registry:

wire internal/registry/wire.go

Note: If you cannot use above command, just install wire tool.

go install github.com/google/wire/cmd/wire@latest

5. Modify cmd/api/main.go

// cmd/api/main.go
package main

import (
"log"

"github.com/hienvl125/todo-api/internal/registry"
"github.com/hienvl125/todo-api/internal/util/config"
)

func main() {
conf, err := config.LoadConfig()
if err != nil {
log.Fatalln(err)
}

ginServer, err := registry.NewGinServer(conf)
if err != nil {
log.Fatalln(err)
}

if err := ginServer.Run(conf.GinPort); err != nil {
log.Fatalln(err)
}
}

Conclusion

You’ve completed Part Two of our tutorial! To sum up what we’ve done:

  • We organized our APIs into layers using clean architecture.
  • Created APIs for listing and adding todos.
  • Made things even easier with google/wire for handling dependencies.

Now, get ready for the next part: deploying our app to render.com. Stay tuned for more exciting steps ahead!

--

--