In this guide we are are going to check a simple implementation Revel Framework REST API. In this tutorial we create will be a simple posts CRUD app that will connect to the Postgres Database. Feel free to checkout the repo where the full code is hosted here.
Before proceeding, ensure that golang is nstalled.
Related Content
- How to install Go in Fedora and Rocky Linux/Centos/RHEL
- How to install Go (Golang) in Arch Linux/Manjaro
- Managing Database migrations with Golang goose using incremental SQL changes
Installing Revel
Next we need to install the revel framework. Revel is a golang framework, it can be installed as a golang library.
To get the Revel framework, use this command?
go get github.com/revel/revel
This command does a couple of things:
- Go uses git to clone the repository into
$GOPATH/src/github.com/revel/revel/
- Go transitively finds all of the dependencies and runs
go get
on them as well.
Next, get and build the revel command line tool
The revel
command line tool is used to build
, run
, and package
Revel applications.
Use go get
to install the revel command line tool:
go get github.com/revel/cmd/revel
Ensure the $GOPATH/bin
directory is in your PATH so that you can reference the command from anywhere.
export PATH="$PATH:$GOPATH/bin"
Verify that revel works:
$ revel
Usage:
revel [OPTIONS] <command>
Application Options:
-v, --debug If set the logger is set to verbose
--historic-run-mode If set the runmode is passed a string not json
--historic-build-mode If set the code is scanned using the original parsers,
not the go.1.11+
-X, --build-flags= These flags will be used when building the application.
May be specified multiple times, only applicable for
Build, Run, Package, Test commands
--gomod-flags= These flags will execute go mod commands for each flag,
this happens during the build process
Available commands:
build
clean
new
package
run
test
version
Creating our application Database
We will store our data in postgres database. So before proceeding, we will need a posgres database and some credentials to connect. Ensure that you have a postges databases created with a user and a password that can connect to it.
Now create a database table that we will use. Checkout better way of using db migrations to manage database operations here
create table posts (
id bigserial primary key,
username varchar(255) not null,
title varchar(100) not null,
content text not null,
created_at timestamptz not null default clock_timestamp(),
updated_at timestamptz
);
Creating Revel app
Create a revel application using this command. Here we are creating an app called
go-revel-crud. The app will be created in the current directory. This command will auto generate _boilerplate _like `app, conf, message, public, test, utils` for the app that we are creating.➜ revel new -a go-revel-crud
Revel executing: create a skeleton Revel application
Your application has been created in:
/Users/etowett/Proj/me/go-revel-crud
You can run it with:
revel run -a go-revel-crud
Next, change to the application directory and run the go mod tidy to download the initial application dependencies.
cd go-revel-crud
❯ go mod tidy
go: finding module for package github.com/garyburd/redigo/redis
go: finding module for package github.com/patrickmn/go-cache
go: finding module for package github.com/bradfitz/gomemcache/memcache
go: found github.com/bradfitz/gomemcache/memcache in github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
go: found github.com/garyburd/redigo/redis in github.com/garyburd/redigo v1.6.2
go: found github.com/patrickmn/go-cache in github.com/patrickmn/go-cache v2.1.0+incompatible
After that, open your browser and navigate to localhost:9000
. You will see IT Works! in your browser, that means that your revel app already running. But since we want a REST CRUD API, We will proceed to customize the app in our case.
Routes
Let us create the routes that we will need for our app. URL’s and routes are defined in the conf/routes
file and have three columns as example below:
[METHOD] [URL Pattern] [Controller.Method]
GET / MySite.Welcome
By default it has some routes added, so you can figure out how it works. Add these routes to the file conf/routes
GET /health App.Health
GET /posts Post.List
GET /post/:id Post.Get
POST /post Post.Add
PUT /post/:id Post.Update
DELETE /post/:id Post.Delete
Controllers
The revel.Controller is the context for a single request and controls
Let us create Posts controller functions for out code.It should be located on <app_root_directory>/app/controllers
. We will call it posts.go.
We need to add “package controller” in the beginning of our code, and by default all controllers should extend a class Controller located on Revel’s library. The controller name needs to be the same declared on routes file. Now we should make a method to render the actions defined in the routes. Revel pages can receive as many attributes as you want.
For our case, let us define controller functions for all our routes:
package controllers
import (
"fmt"
"go-revel-crud/app/db"
"go-revel-crud/app/entities"
"go-revel-crud/app/forms"
"go-revel-crud/app/models"
"go-revel-crud/app/webutils"
"net/http"
"strings"
"github.com/revel/revel"
)
type Post struct {
App
}
func (c Post) List() revel.Result {
var result entities.Response
ctx := c.Request.Context()
paginationFilter, err := webutils.FilterFromQuery(c.Params)
if err != nil {
c.Log.Errorf("could not filter from params: %v", err)
result = entities.Response{
Success: false,
Status: http.StatusBadRequest,
Message: "Failed to parse page filters",
}
return c.RenderJSON(result)
}
newPost := &models.Post{}
data, err := newPost.All(ctx, db.DB(), paginationFilter)
if err != nil {
c.Log.Errorf("could not get posts: %v", err)
result = entities.Response{
Success: false,
Status: http.StatusInternalServerError,
Message: "Could not get posts",
}
return c.RenderJSON(result)
}
recordsCount, err := newPost.Count(c.Request.Context(), db.DB(), paginationFilter)
if err != nil {
c.Log.Errorf("could not get posts count for listing: %v", err)
result = entities.Response{
Success: false,
Status: http.StatusInternalServerError,
Message: "Could not get posts count",
}
return c.Render(result)
}
result = entities.Response{
Success: true,
Status: http.StatusOK,
Data: map[string]interface{}{
"posts": data,
"pagination": models.NewPagination(recordsCount, paginationFilter.Page, paginationFilter.Per),
},
}
return c.RenderJSON(result)
}
func (c Post) Get(id int64) revel.Result {
result := &entities.Response{}
ctx := c.Request.Context()
newPost := &models.Post{}
post, err := newPost.ByID(ctx, db.DB(), id)
if err != nil {
c.Log.Errorf("could not get post: %v", err)
result.Success = false
result.Message = "Could not get the post"
result.Data = post
return c.RenderJSON(result)
}
result.Success = true
result.Data = post
return c.RenderJSON(result)
}
func (c Post) Add() revel.Result {
var status int
postForm := forms.Post{}
err := c.Params.BindJSON(&postForm)
if err != nil {
c.Log.Errorf("could not bind create post form to json: %v", err)
status = http.StatusBadRequest
c.Response.SetStatus(status)
return c.RenderJSON(entities.Response{
Message: "There was a problem with the form you submitted",
Status: status,
Success: false,
})
}
ctx := c.Request.Context()
v := c.Validation
postForm.Validate(v)
if v.HasErrors() {
retErrors := make([]string, 0)
for _, theErr := range v.Errors {
retErrors = append(retErrors, theErr.Message)
}
status = http.StatusBadRequest
c.Response.SetStatus(status)
return c.RenderJSON(entities.Response{
Message: strings.Join(retErrors, ","),
Status: status,
Success: false,
})
}
newPost := &models.Post{
Username: postForm.Username,
Title: postForm.Title,
Content: postForm.Content,
}
err = newPost.Save(ctx, db.DB())
if err != nil {
c.Log.Errorf("could not save post: %v", err)
status = http.StatusInternalServerError
c.Response.SetStatus(status)
return c.RenderJSON(entities.Response{
Message: "Encountered an error saving request.",
Status: status,
Success: false,
})
}
status = http.StatusCreated
c.Response.SetStatus(status)
return c.RenderJSON(entities.Response{
Data: newPost,
Status: status,
Success: true,
})
}
func (c *Post) Update(id int64) revel.Result {
var status int
data := models.Post{}
err := c.Params.BindJSON(&data)
if err != nil {
c.Log.Errorf("could not bind update post form to json: %v", err)
status = http.StatusBadRequest
c.Response.SetStatus(status)
return c.RenderJSON(entities.Response{
Message: "There was a problem with the form you submitted",
Status: status,
Success: false,
})
}
data.ID = id
err = data.Save(c.Request.Context(), db.DB())
if err != nil {
c.Log.Errorf("could not save post: %v", err)
status = http.StatusInternalServerError
c.Response.SetStatus(status)
return c.RenderJSON(entities.Response{
Message: "Encountered an error saving request.",
Status: status,
Success: false,
})
}
status = http.StatusOK
c.Response.SetStatus(status)
return c.RenderJSON(entities.Response{
Data: data,
Status: status,
Success: true,
})
}
func (c *Post) Delete(id int64) revel.Result {
var status int
ctx := c.Request.Context()
data := models.Post{}
data.ID = id
_, err := data.Delete(ctx, db.DB())
if err != nil {
c.Log.Errorf("could not delete post: %v", err)
status = http.StatusInternalServerError
c.Response.SetStatus(status)
return c.RenderJSON(entities.Response{
Message: "Encountered an error deleting the post",
Status: status,
Success: false,
})
}
status = http.StatusOK
c.Response.SetStatus(status)
return c.RenderJSON(entities.Response{
Status: status,
Success: true,
Message: fmt.Sprintf("post id =[%v] deleted", id),
})
}
Models
For our models, we will create functions that will interact with the database to execute the db queries for the respective functions that we want to achieve. We will have functions to do create records, update, fetch and delete.
Create the models for our posts crud app:
package models
import (
"context"
"fmt"
"go-revel-crud/app/db"
"go-revel-crud/app/helpers"
"strings"
)
const (
createPostSQL = `insert into posts (username, title, content, created_at) values ($1, $2, $3, $4) returning id`
getPostSQL = `select id, username, title, content, created_at, updated_at from posts`
getPostByID = getPostSQL + ` where id=$1`
updatePostSQL = `update posts set (username, title, content, updated_at) = ($1, $2, $3, $4) where id = $5`
countPostSQL = `select count(id) from posts`
deletePostSQL = `delete from posts where id=$1`
)
type (
Post struct {
SequentialIdentifier
Username string `json:"username"`
Title string `json:"title"`
Content string `json:"content"`
Timestamps
}
)
func (p *Post) All(
ctx context.Context,
db db.SQLOperations,
filter *Filter,
) ([]*Post, error) {
posts := make([]*Post, 0)
query, args := p.buildQuery(
getPostSQL,
filter,
)
rows, err := db.QueryContext(
ctx,
query,
args...,
)
defer rows.Close()
if err != nil {
return posts, err
}
for rows.Next() {
var post Post
err = rows.Scan(
&post.ID,
&post.Username,
&post.Title,
&post.Content,
&post.CreatedAt,
&post.UpdatedAt,
)
if err != nil {
return posts, err
}
posts = append(posts, &post)
}
return posts, err
}
func (q *Post) Count(
ctx context.Context,
db db.SQLOperations,
filter *Filter,
) (int, error) {
query, args := q.buildQuery(
countPostSQL,
&Filter{
Term: filter.Term,
},
)
var recordsCount int
err := db.QueryRowContext(ctx, query, args...).Scan(&recordsCount)
return recordsCount, err
}
func (p *Post) Delete(
ctx context.Context,
db db.SQLOperations,
) (int64, error) {
res, err := db.ExecContext(ctx, deletePostSQL, p.ID)
if err != nil {
return 0, err
}
rowsDeleted, err := res.RowsAffected()
if err != nil {
return 0, err
}
return rowsDeleted, nil
}
func (p *Post) ByID(
ctx context.Context,
db db.SQLOperations,
id int64,
) (*Post, error) {
row := db.QueryRowContext(ctx, getPostByID, id)
return p.scan(row)
}
func (p *Post) Save(
ctx context.Context,
db db.SQLOperations,
) error {
p.Timestamps.Touch()
var err error
if p.IsNew() {
err := db.QueryRowContext(
ctx,
createPostSQL,
p.Username,
p.Title,
p.Content,
p.Timestamps.CreatedAt,
).Scan(&p.ID)
return err
}
_, err = db.ExecContext(
ctx,
updatePostSQL,
p.Username,
p.Title,
p.Content,
p.Timestamps.UpdatedAt,
p.ID,
)
return err
}
func (*Post) scan(
row db.RowScanner,
) (*Post, error) {
var p Post
err := row.Scan(
&p.ID,
&p.Username,
&p.Title,
&p.Content,
&p.CreatedAt,
&p.UpdatedAt,
)
return &p, err
}
func (p *Post) buildQuery(
query string,
filter *Filter,
) (string, []interface{}) {
conditions := make([]string, 0)
args := make([]interface{}, 0)
placeholder := helpers.NewPlaceholder()
if filter.Term != "" {
likeStmt := make([]string, 0)
columns := []string{"username", "title", "content"}
for _, col := range columns {
search := fmt.Sprintf(" (lower(%s) like '%%' || $%d || '%%')", col, placeholder.Touch())
likeStmt = append(likeStmt, search)
args = append(args, filter.Term)
}
conditions = append(conditions, fmt.Sprintf("(%s)", strings.Join(likeStmt, " or")))
}
if len(conditions) > 0 {
query += " where" + strings.Join(conditions, " and")
}
if filter.Per > 0 && filter.Page > 0 {
query += fmt.Sprintf(" order by id desc limit $%d offset $%d", placeholder.Touch(), placeholder.Touch())
args = append(args, filter.Per, (filter.Page-1)*filter.Per)
}
return query, args
}
Other logic
We could also have code for the following things in our code:
- Views – defines the html content to be rendered to the browser parsing the data. By default they are located on: /<controller_name>/
. You will need to create the Apps directory on: <app_root_directory>/app/views. - Tests – Revel has a place to tests, it is located on <app_root_directory>/tests
Runing the app
Once the app is ready, we can run it using the
revel run .
command in the same directory. But before doing that, we need to set the application Database URL and the PORT.
❯ export DB_URL=postgres://go-revel-crud:go-revel-crud@127.0.0.1:5432/go-revel-crud?sslmode=disable
<meta charset="utf-8">❯ export PORT=8090
<meta charset="utf-8">❯ revel run .
Revel executing: run a Revel application
Changed detected, recompiling
Parsing packages, (may require download if not cached)... Completed
INFO 10:56:59 app run.go:34: Running revel server
INFO 10:56:59 app plugin.go:9: Go to /@tests to run the tests.
INFO 10:56:59 app revel_logger.go:29: DB Connected Successfully
Revel engine is listening on.. 0.0.0.0:50065
Revel proxy is listening, point your browser to : 8090
Time to recompile 4.931984424s
Testing the application
Verify that the healthcheck endpoint is working with this curl command?
➜ curl http://127.0.0.1:8090/health
{
"build_time": "2021-10-29T07:56:56Z",
"db": {
"hello": "1",
"type": "postgres",
"up": true,
"version": "PostgreSQL 14.0 on x86_64-pc-linux-musl, compiled by gcc (Alpine 10.3.1_git20210424) 10.3.1 20210424, 64-bit"
},
"server": {
"compiler": "gc",
"cpu": 12,
"goarch": "amd64",
"goos": "darwin",
"goroutines": 11,
"hostname": "eutychus-pc.local",
"memory": {
"alloc": "1 MB",
"num_gc": 1,
"sys": "18 MB",
"total_alloc": "2 MB"
}
},
"status": 200,
"success": true,
"time": {
"now": "2021-10-29T10:58:15.161494+03:00",
"offset": 10800,
"timezone": "EAT"
},
"version": "git-8e73d25-dirty"
}
Add post endpoint {.wp-block-heading}
Next verify the Add post endpoint:
curl -X POST http://127.0.0.1:8090/post \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"username": "eutychus",
"title": "Post One",
"content": "This is the first post"
}'
Response:
{
"success": true,
"status": 201,
"message": "",
"data": {
"id": 1,
"username": "eutychus",
"title": "Post One",
"content": "This is the first post",
"created_at": "2021-10-29T11:11:23.50606+03:00",
"updated_at": null
}
List Posts endpoint
curl http://127.0.0.1:8090/posts
Response:
{
"success": true,
"status": 200,
"message": "",
"data": {
"pagination": {
"count": 2,
"has_next_page": false,
"has_prev_page": false,
"next_page": null,
"num_pages": 1,
"page": 1,
"per": 20,
"prev_page": null
},
"posts": [
{
"id": 2,
"username": "eutychus",
"title": "Post Two",
"content": "This is the second post",
"created_at": "2021-10-29T08:15:31.026777Z",
"updated_at": null
},
{
"id": 1,
"username": "eutychus",
"title": "Post One",
"content": "This is the first post",
"created_at": "2021-10-29T08:11:23.50606Z",
"updated_at": null
}
]
}
}
Get single post
curl http://127.0.0.1:8090/post/1
Response:
{
"success": true,
"status": 0,
"message": "",
"data": {
"id": 1,
"username": "eutychus",
"title": "Post One",
"content": "This is the first post",
"created_at": "2021-10-29T08:11:23.50606Z",
"updated_at": null
}
}
Update post
curl -X PUT http://127.0.0.1:8090/post/1 \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"username": "eutychus",
"title": "Post One - Updated",
"content": "This is the first post - Updated"
}'
Response:
{
"success": true,
"status": 200,
"message": "",
"data": {
"id": 1,
"username": "eutychus",
"title": "Post One - Updated",
"content": "This is the first post - Updated",
"created_at": "2021-10-29T11:22:00.507955+03:00",
"updated_at": null
}
}
Delete Post
curl -X DELETE http://127.0.0.1:8090/post/1
Response:
{
"success": true,
"status": 200,
"message": "post id =[1] deleted",
"data": null
}
Conclusion:
In this post we managed to explore how to do CRUD with golang revel. Checkout full code in this repo here.