Building a REST CRUD app with Golang Revel

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

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?

1
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:

1
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.

1
export PATH="$PATH:$GOPATH/bin"

Verify that revel works:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ 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

1
2
3
4
5
6
7
8
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.

1
2
3
4
5
6
7
8
$ 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.

1
cd go-revel-crud
1
2
3
4
5
6
7
8
$ 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:

1
2
[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

1
2
3
4
5
6
7
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

  • the incoming Request stuff
  • and the Response back, in Html, Json, Xml, File or your own custom.

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:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
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:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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>/<action>. 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ export DB_URL=postgres://go-revel-crud:go-revel-crud@127.0.0.1:5432/go-revel-crud?sslmode=disable
$ export PORT=8090
$ 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?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ 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

Next verify the Add post endpoint:

1
2
3
4
5
6
7
8
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "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

1
curl http://127.0.0.1:8090/posts

Response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
  "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

1
curl http://127.0.0.1:8090/post/1

Response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "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

1
2
3
4
5
6
7
8
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "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

1
curl -X DELETE http://127.0.0.1:8090/post/1

Response:

1
2
3
4
5
6
{
  "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.

comments powered by Disqus
Citizix Ltd
Built with Hugo
Theme Stack designed by Jimmy