
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.
Prerequisites
Before we get started, you will need to have the following installed on your machine:
- Go 1.16 or higher
- PostgreSQL 13 or higher
- A text editor or IDE of your choice
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.
mkdir fitness-api
cd fitness-api
go mod init fitness-api
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.
go get -u -d github.com/labstack/echo/v4
Installing the PostgreSQL Driver
Next, we will install the PostgreSQL driver for Go.
go get -u -d github.com/lib/pq
Installing the Echo Middleware
Next, we will install the Echo middleware for Go.
go get -u -d github.com/labstack/echo/v4/middleware
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.
psql
create database fitness;
\c fitness
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.
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
);
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.
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)
);
To check if the tables were created successfully, we will use the \dt
command to list all the tables in the database.
\dt
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.
package main
import (
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.Logger.Fatal(e.Start(":8080"))
}
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.
go run main.go
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.10.0
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
O\
⇨ http server started on [::]:8080
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.
package handlers
import (
"net/http"
"github.com/labstack/echo/v4"
)
func Home(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
Remember to run
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.
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"))
}
On running the go run
command, we will get the Hello, World!
message on the browser.
go run main.go
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.
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"`
}
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.
go get github.com/joho/godotenv
go mod tidy
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.
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=fitness
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.
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
}
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.
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"))
}
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.
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
}
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.
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)
}
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.
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"))
}
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"
}
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"
}
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.
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
}
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.
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)
}
Creating the Measurements Route
In the main.go
file, we will call the CreateMeasurement()
function.
...
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)
...
}
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
}
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"
}
Update the User
To update the user create a new function called UpdateUser
in the usersDb.go
file.
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
}
Create a new function called HandleUpdateUser
in the handleUsers.go
file.
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)
}
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.
...
func main(){
...
e.PUT("/users/:id", handlers.handleUpdateUser)
...
}
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"
}
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.
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
}
Create a new function called HandleUpdateMeasurement
in the handleMeasurements.go
file.
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)
}
In the main.go
file, we will call the HandleUpdateMeasurement()
function.
...
func main(){
...
e.PUT("/measurements/:id", handlers.HandleUpdateMeasurement)
...
}
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
}
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.
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
}
}
In the main.go
file, we will call the LogRequest()
function.
...
func main(){
...
e.Use(handlers.LogRequest)
...
}
Run the application and send a request to the http://localhost:8080/users
endpoint. You should see the request details in the terminal.
Request: GET /users 1.0001ms 200
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.
...
import (
"github.com/labstack/echo/v4/middleware"
)
...
func main(){
...
e.Use(middleware.Logger())
...
}
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 RoutesCORs 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.
...
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"http://localhost:3000"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
}))
...
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.
...
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
}))
...
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.