Last updated on
author
Kevin Kelche

Golang Echo Tutorial: REST API with PostgreSQL


Welcome to this tutorial on creating a fitness API using the Echo framework in Go. Echo is a high-performance web framework designed for building RESTful APIs in the Go programming language. It is known for its speed and lightweight nature, making it an excellent choice for web development. In this tutorial, we’ll guide you through the process of using Echo to construct a RESTful API for storing and managing fitness-related information, such as weight, height, and body fat percentage, in a PostgreSQL database.

Throughout this tutorial, we will cover essential concepts in using Echo, including routing, middleware, and handling HTTP requests and responses. We will demonstrate how to perform CRUD (Create, Read, Update, Delete) operations to manage user data effectively within the API. Whether you are a novice or an experienced Go developer, this tutorial will equip you with the knowledge and skills to build a robust fitness API using the Echo framework.

Let’s begin our journey into creating a simple yet powerful fitness API with Echo, allowing you to develop powerful web applications for fitness tracking and management.

Master Go by building

Redis

from scratch

Prerequisites

Before we get started, you will need to have the following installed on your machine:

View the source code on GitHub

Setting up the Project

To get started, we will create a new directory for our project and initialize a new Go module. We will use the go mod init command to initialize a new Go module and the go mod tidy command to download and install the required dependencies.

Terminal
mkdir fitness-api
cd fitness-api
go mod init fitness-api

Copied!

Installing Echo

Next, we will install the Echo framework using the go get command. We will use the -u flag to update the dependencies and the -d flag to download the dependencies without installing them.

Terminal
go get -u -d github.com/labstack/echo/v4

Copied!

Installing the PostgreSQL Driver

Next, we will install the PostgreSQL driver for Go.

Terminal
go get -u -d github.com/lib/pq

Copied!

Installing the Echo Middleware

Next, we will install the Echo middleware for Go.

Terminal
go get -u -d github.com/labstack/echo/v4/middleware

Copied!

Creating the Database

Enter the PostgreSQL shell using the psql command. We will then create a new database called fitness using the CREATE DATABASE query. We will then connect to the fitness database using the \c command.

Terminal
psql
create database fitness;
\c fitness

Copied!

Creating the Users Table

We will create a new table called users to store the user’s information. We will use the CREATE TABLE command to create a new table. The users table will have a primary key called id that will be auto-incremented. We will also add a name, email, and password column to store the user’s information. Finally, we will add a created_at and updated_at columns to store the date and time when the user was created and updated.

psql
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  email VARCHAR(255) NOT NULL,
  password VARCHAR(255) NOT NULL,
  created_at TIMESTAMP NOT NULL,
  updated_at TIMESTAMP NOT NULL
);

Copied!

Creating the Measurements Table

We will then create a new table called measurements to store the user’s fitness measurements. We will use the CREATE TABLE command to create a new table. The measurements table will have a foreign key to the users table.

psql
CREATE TABLE measurements (
  id SERIAL PRIMARY KEY,
  user_id INTEGER NOT NULL,
  weight FLOAT NOT NULL,
  height FLOAT NOT NULL,
  body_fat FLOAT NOT NULL,
  created_at TIMESTAMP NOT NULL,
  FOREIGN KEY (user_id) REFERENCES users(id)
);

Copied!

To check if the tables were created successfully, we will use the \dt command to list all the tables in the database.

Terminal
\dt

Copied!

Creating the API

Create a new file called main.go in the root directory of the project. We will then import the required packages and initialize the Echo framework.

fitness-api/main.go
package main

import (
  "github.com/labstack/echo/v4"
)

func main() {
  e := echo.New()
  e.Logger.Fatal(e.Start(":8080"))
}

Copied!

This will start the Echo server on port 8080. We can now test the server by running the go run command, then navigating to http://localhost:8080 in our browser.

Terminal
go run main.go

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.10.0
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
http server started on [::]:8080

Copied!

As you can see, we get a 404 page not found error. This is because we have not defined any routes yet. Let’s define a new route to handle the / endpoint.

Create a new directory called cmd in the root directory of the project. We will then create a new directory called handlers inside the cmd directory. We will then create a new file called rootHandler.go inside the handlers directory.

In the handlers.go file, we will define a new function called Home that will handle the / endpoint.

fitness-api/cmd/handlers/rootHandler.go
package handlers

import (
  "net/http"

  "github.com/labstack/echo/v4"
)

func Home(c echo.Context) error {
  return c.String(http.StatusOK, "Hello, World!")
}

Copied!

Note:

Remember to run go mod tidy after adding a new package to the project.

The Home function takes in an echo.Context as a parameter. The echo.Context contains the request and response objects.

In this function, we are returning a 200 OK status code and a Hello, World! message.

Next, we will import the handlers package in the main.go file and register the Home handler to the / endpoint.

fitness-api/main.go
import (
  "github.com/labstack/echo/v4"
  "fitness-api/cmd/handlers"
)

func main() {
  e := echo.New()
  e.GET("/", handlers.Home)
  e.Logger.Fatal(e.Start(":8080"))
}

Copied!

On running the go run command, we will get the Hello, World! message on the browser.

Terminal
go run main.go

Copied!

A lot is happening in the main.go file. Let’s break it down. First, we are importing the github.com/labstack/echo/v4 package. This is the Echo framework that we installed earlier. Next, we are importing the handlers package that we created earlier. We are then initializing the Echo framework using the echo.New() function. We are then registering the Home handler to the / endpoint using the e.GET() function. Finally, we are starting the Echo server using the e.Start() function.

Creating the User and Measurement Models

Create a new directory called models in the cmd directory. We will then create a new file called user.go inside the models directory.

This file will contain the User model. The User model will have the ID, Name, Email, Password, CreatedAt, and UpdatedAt fields.

The Measurements model will have the ID, UserId, Weight, Height, BodyFat, and Date fields.

fitness-api/cmd/models/user.go
package models

import "time"

type User struct {
  Id        int       `json:"id"`
  Name      string    `json:"name"`
  Email     string    `json:"email"`
  Password  string    `json:"password"`
  CreatedAt time.Time `json:"created_at"`
  UpdatedAt time.Time `json:"updated_at"`
}

type Measurements struct {
  Id        int         `json:"id"`
  UserId    int         `json:"user_id"`
  Weight    float64     `json:"weight"`
  Height    float64     `json:"height"`
  BodyFat   float64     `json:"body_fat"`
  Created_at time.Time  `json:"created_at"`
}

Copied!

These structs will be used to map the database tables to Go structs. Think of them as Golang representations of the database tables.

Dotenv for the database creatdetials

Install the github.com/joho/godotenv package using the go get command.

Terminal
go get github.com/joho/godotenv
go mod tidy

Copied!

create a new file called .env in the root directory of the project. We will then add the database connection string to the .env file.

fitness-api/.env
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=fitness

Copied!

Connecting to the database

Create a new folder called storage in the cmd directory. We will then create a new file called db.go inside the storage directory.

In the db.go file, we will define a new function called Connect that will connect to the database.

fitness-api/cmd/storage/db.go
package storage

import (
  "database/sql"
  "fmt"
  "log"
  "os"

  "github.com/joho/godotenv"
  _ "github.com/lib/pq"
)


var db *sql.DB

func InitDB() {
  err := godotenv.Load()
  if err != nil {
    log.Fatal("Error loading .env file")
  }

  dbHost := os.Getenv("DB_HOST")
  dbPort := os.Getenv("DB_PORT")
  dbUser := os.Getenv("DB_USER")
  dbPass := os.Getenv("DB_PASSWORD")
  dbName := os.Getenv("DB_NAME")

  db, err = sql.Open("postgres", fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", dbHost, dbUser, dbPass, dbName, dbPort))

  if err != nil {
    panic(err.Error())
  }

  err = db.Ping()
  if err != nil {
    panic(err.Error())
  }

  fmt.Println("Successfully connected to database")
}

func GetDB() *sql.DB{
    return db
}

Copied!

In this function, we are using the sql.Open() function to connect to the database. The sql.Open() function takes two parameters. The first parameter is the database driver name in this case, postgres. The second parameter is the database connection string. The database connection string contains the database host, port, username, password, database name, and other connection parameters. We are using the fmt.Sprintf() function to format the database connection string. We are then using the db.Ping() function to check if the database connection is successful.

Next, we will import the storage package in the main.go file and call the InitDB() function.

fitness-api/main.go
import (
  "github.com/labstack/echo/v4"
  "fitness-api/cmd/handlers"
  "fitness-api/cmd/storage"
)

func main() {
  e := echo.New()
  e.GET("/", handlers.Home)
  // Add this line
  storage.InitDB()
  //----------------
  e.Logger.Fatal(e.Start(":8080"))
}

Copied!

On running the go run . command, we will get the Successfully connected to database message on the terminal.

CRUD Operations

Creating the User Repository

Create a new directory called repositories in the cmd directory. We will then create a new file called userDb.go inside the repositories directory.

In the userDb.go file, we will define a new function called CreateUser that will create a new user in the database.

fitness-api/cmd/repositories/userDb.go
package repositories

import (
  "fitness-api/cmd/models"
  "fitness-api/cmd/storage"
)

func CreateUser(user models.User) (models.User, error) {
  db := storage.GetDB()
  sqlStatement := `INSERT INTO users (name, email, password) VALUES ($1, $2, $3) RETURNING id`
  err := db.QueryRow(sqlStatement, user.Name, user.Email, user.Password).Scan(&user.Id)
  if err != nil {
    return user, err
  }
  return user, nil
}

Copied!

In this function, we are using the storage.GetDB() function to get the database connection. We are then using the db.QueryRow() function to execute the SQL query. The db.QueryRow() function takes two parameters. The first parameter is the SQL query. The second parameter is the query parameters. The db.QueryRow() function returns a sql.Row object. We are then using the sql.Row.Scan() function to scan the returned row and assign the value to the user.Id field.

Creating the User Handler

Create a new file called handleUsers.go inside the handlers directory. We will then define a new function called CreateUser that will create a new user in the database.

fitness-api/cmd/handlers/handleUsers.go
package handlers

import (
  "fitness-api/cmd/models"
  "fitness-api/cmd/repositories"
  "net/http"

  "github.com/labstack/echo/v4"
)

func CreateUser(c echo.Context) error {
  user := models.User{}
  c.Bind(&user)
  newUser, err := repositories.CreateUser(user)
  if err != nil {
    return c.JSON(http.StatusInternalServerError, err.Error())
  }
  return c.JSON(http.StatusCreated, newUser)
}

Copied!

In this function, we are using the c.Bind() function to bind the request body to the user variable. We are then using the repositories.CreateUser() function to create a new user in the database. If an error occurs, we are returning a 500 status code with the error message. If the user is created successfully, we are returning a 201 status code with the created user.

Next, we will import the handlers package in the main.go file and call the CreateUser() function.

fitness-api/main.go
import (
  "github.com/labstack/echo/v4"
  "fitness-api/cmd/handlers"
  "fitness-api/cmd/storage"
  "fitness-api/cmd/repositories"
)

func main() {
  e := echo.New()
  e.GET("/", handlers.Home)
  storage.InitDB()
  // Add this line
  e.POST("/users", handlers.CreateUser)
  //----------------
  e.Logger.Fatal(e.Start(":8080"))
}

Copied!

On running the go run . command, we will get the Successfully connected to database message on the terminal.

If you are using Postman or a similar API testing platform, you can test the API by sending a POST request to the http://localhost:8080/users endpoint with the following request body.

{
  "name": "John Doe",
  "email": "[email protected]",
  "password": "password"
}

Copied!

If the user is created successfully, you will get the following response.

{
  "id": 1,
  "name": "John Doe",
  "email": "[email protected]",
  "password": "password",
  "created_at": "0001-01-01T00:00:00Z",
  "updated_at": "0001-01-01T00:00:00Z"
}

Copied!

Creating the Measurements Repository

Create a new file called measurementsDb.go inside the repositories directory. We will then define a new function called CreateMeasurement that will create a new measurement in the database.

fitness-api/cmd/repositories/measurementsDb.go
package repositories

import (
  "fitness-api/cmd/models"
  "fitness-api/cmd/storage"
  "time"
)

func CreateMeasurement(measurement models.Measurements) (models.Measurements, error) {
  db := storage.GetDB()
  sqlStatement := `INSERT INTO measurements (user_id, weight, height, body_fat, created_at) VALUES ($1, $2, $3, $4, $5) RETURNING id`
  err := db.QueryRow(sqlStatement, measurement.UserId, measurement.Weight, measurement.Height, measurement.BodyFat, time.Now()).Scan(&measurement.Id)
  if err != nil {
    return measurement, err
  }

  return measurement, nil
}

Copied!

Creating the Measurements Handler

Create a new file called handleMeasurements.go inside the handlers directory. We will then define a new function called CreateMeasurement that will create a new measurement in the database.

fitness-api/cmd/handlers/handleMeasurements.go
package handlers

import (
  "fitness-api/cmd/models"
  "fitness-api/cmd/repositories"
  "net/http"

  "github.com/labstack/echo/v4"
)

func CreateMeasurement(c echo.Context) error {
  measurement := models.Measurements{}
  c.Bind(&measurement)
  newMeasurement, err := repositories.CreateMeasurement(measurement)
  if err != nil {
    return c.JSON(http.StatusInternalServerError, err.Error())
  }
  return c.JSON(http.StatusCreated, newMeasurement)
}

Copied!

Creating the Measurements Route

In the main.go file, we will call the CreateMeasurement() function.

fitness-api/main.go
...
import (
  "github.com/labstack/echo/v4"
  "fitness-api/cmd/handlers"
  "fitness-api/cmd/storage"
  "fitness-api/cmd/repositories"
)

func main(){
...
  e.POST("/measurements", handlers.CreateMeasurement)
...
}

Copied!

On running the go run . command then send a POST request to the http://localhost:8080/measurements endpoint with the following request body.

{
  "user_id": 1,
  "weight": 80,
  "height": 180,
  "body_fat": 20
}

Copied!

If the measurement is created successfully, you will get the following response.

{
  "id": 1,
  "user_id": 1,
  "weight": 80,
  "height": 180,
  "body_fat": 20,
  "created_at": "0001-01-01T00:00:00Z"
}

Copied!

Update the User

To update the user create a new function called UpdateUser in the usersDb.go file.

fitness-api/cmd/repositories/usersDb.go
func UpdateUser(user models.User, id int) (models.User, error) {
  db := storage.GetDB()
  sqlStatement := `
    UPDATE users
    SET name = $2, email = $3, password = $4, updated_at = $5
    WHERE id = $1
    RETURNING id`
  err := db.QueryRow(sqlStatement, id, user.Name, user.Email, user.Password, time.Now()).Scan(&id)
  if err != nil {
    return models.User{}, err
  }
  user.Id = id
  return user, nil
}

Copied!

Create a new function called HandleUpdateUser in the handleUsers.go file.

fitness-api/cmd/handlers/handleUsers.go
func HandleUpdateUser(c echo.Context) error {
  id := c.Param("id")

  idInt, err := strconv.Atoi(id)
  if err != nil {
    return c.JSON(http.StatusInternalServerError, err.Error())
  }

  user := models.User{}
  c.Bind(&user)
  updatedUser, err := repositories.UpdateUser(user, idInt)
  if err != nil {
    return c.JSON(http.StatusInternalServerError, err.Error())
  }
  return c.JSON(http.StatusOK, updatedUser)
}

Copied!

This function will take the id parameter from the URL and use it to update the user in the database. We have to convert the id parameter to an integer before we can use it to update the user.

In the main.go file, we will call the handleUpdateUser() function.

fitness-api/main.go
...
func main(){
...
  e.PUT("/users/:id", handlers.handleUpdateUser)
...
}

Copied!

Choose a user you’d like to update and send a PUT request to the http://localhost:8080/users/:id endpoint with the following request body.

{
  "name": "Jane Wanjiru",
  "email": "[email protected]",
  "password": "34jlse9,3"
}

Copied!

This will update the user with the id you specified in the URL.

We have covered the basic CRUD operations for the user. You can try to implement the other CRUD operations for the user. such as deleting a user, getting a user, and getting all users.

Update the Measurement

To update the measurement create a new function called UpdateMeasurement in the measurementsDb.go file.

fitness-api/cmd/repositories/measurementsDb.go
func UpdateMeasurement(measurement models.Measurements, id int) (models.Measurements, error) {
  db := storage.GetDB()
  sqlStatement := `
    UPDATE measurements
    SET weight = $2, height = $3, body_fat = $4, created_at = $5
    WHERE id = $1
    RETURNING id`
  err := db.QueryRow(sqlStatement, id, measurement.Weight, measurement.Height, measurement.BodyFat, time.Now()).Scan(&id)
  if err != nil {
    return models.Measurements{}, err
  }
  measurement.Id = id
  return measurement, nil
}

Copied!

Create a new function called HandleUpdateMeasurement in the handleMeasurements.go file.

fitness-api/cmd/handlers/handleMeasurements.go
func HandleUpdateMeasurement(c echo.Context) error {
  id := c.Param("id")

  idInt, err := strconv.Atoi(id)
  if err != nil {
    return c.JSON(http.StatusInternalServerError, err.Error())
  }

  measurement := models.Measurements{}
  c.Bind(&measurement)
  updatedMeasurement, err := repositories.UpdateMeasurement(measurement, idInt)
  if err != nil {
    return c.JSON(http.StatusInternalServerError, err.Error())
  }

  return c.JSON(http.StatusOK, updatedMeasurement)
}

Copied!

In the main.go file, we will call the HandleUpdateMeasurement() function.

fitness-api/main.go
...
func main(){
...
  e.PUT("/measurements/:id", handlers.HandleUpdateMeasurement)
...
}

Copied!

Choose a measurement you’d like to update and send a PUT request to the http://localhost:8080/measurements/:id endpoint with the following request body.

{
  "weight": 80,
  "height": 180,
  "body_fat": 20
}

Copied!

You can continue with the other CRUD operations for the measurement.

Echo Middleware

Let’s now explore the concept of middleware in Echo. Middleware is a piece of code that runs between the request and the response. It is used to perform some actions before the request is handled by the handler. For example, we can use middleware to log the request details, authenticate the user, and validate the request body.

In this section, we will create a middleware that will log the request details.

Create a new file called middleware.go in the handlers folder.

fitness-api/cmd/handlers/middleware.go
package handlers

import (
  "fmt"
  "net/http"
  "time"

  "github.com/labstack/echo/v4"
)

func LogRequest(next echo.HandlerFunc) echo.HandlerFunc {
  return func(c echo.Context) error {
    start := time.Now()
    err := next(c)
    stop := time.Now()
    fmt.Printf("Request: %s %s %s %s\n", c.Request().Method, c.Request().URL, stop.Sub(start), c.Response().Status)
    return err
  }
}

Copied!

In the main.go file, we will call the LogRequest() function.

fitness-api/main.go
...
func main(){
...
  e.Use(handlers.LogRequest)
...
}

Copied!

Run the application and send a request to the http://localhost:8080/users endpoint. You should see the request details in the terminal.

terminal
Request: GET /users 1.0001ms 200

Copied!

LogRequest is a custom middleware function that takes a next function as an argument. The next function is the handler function that will be called after the middleware function. The next function is called by passing the c argument. The c argument is the context object that contains the request and response objects.

You can also use the middlewares provided by Echo. In the main.go file, we will add the logger middleware.

fitness-api/main.go
...
import (
  "github.com/labstack/echo/v4/middleware"
)
...
func main(){
...
  e.Use(middleware.Logger())
...
}

Copied!

The Use() function takes a list of middleware functions as arguments. You can add as many middleware functions as you want.

Also Read:

Echo Group Routes

CORs Middleware

Let’s now add a middleware that will allow us to make requests from the front end. We will use the CORs middleware from Echo.

fitness-api/main.go
...
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
  AllowOrigins: []string{"http://localhost:3000"},
  AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
}))
...

Copied!

This will allow us to make requests from the front end in port 3000. We could also use the AllowOrigins to allow requests from all origins.

fitness-api/main.go
...
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
  AllowOrigins: []string{"*"},
  AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
}))
...

Copied!

Conclusion

In this tutorial, we have covered the basics of the Echo framework. We have created a simple API that allows us to create, read, update, and delete users and measurements. We have also covered the concept of middleware in the Echo framework.

Echo is a great framework for building APIs. It is lightweight and easy to use. You can use it to build your next API or your microservice. Tune in for the next tutorial where we will cover the basics of Docker and how to containerize our API.

Resources

Subscribe to my newsletter

Get the latest posts delivered right to your inbox.