Developing a simple CRUD API with Go, Gin and Gorm

Introduction

Golang is an exciting language but newcomers can be overwhelmed by the new semantics and variety of frameworks available. Getting started with the basics can feel like a challenge.

In this example I wanted to show the most minimal set of code needed to create a functional api. We’ll develop a simple API that provides Create, Read, Update, and Delete (CRUD) functions for a basic model. Using and Object Relationship Mapping (ORM) tool we’ll be able to quickly update our data model with new fields all under 100 lines of code. So let’s get started.

The final code for this walk through can be found at https://github.com/cgrant/gin-gorm-api-example

Getting Started

This example assumes you already have Go installed and running. If you still need to get setup, head over to http://cgrant.io/tutorials/go/getting-started-with-go/ for a quick primer.

Web Framework with Gin

Since we’ll be serving our API over HTTP we’ll need a web framework to handle routing and serving the requests. There are many frameworks available with different features and performance gains. For this example we’ll be using the Gin Web Framework https://github.com/gin-gonic/gin . Gin is a nice framework for API development due to it’s speed and simplicity.

To start, let’s create a new folder for our service at $GOPATH/src/simple-api and add a main.go file as follows

package main
import “fmt”
func main() {
fmt.Println(“Hello World”)
}

Before we go too far let’s test it out to ensure everything is running correctly.

$ go run main.go
Hello World

Perfect we’re all set. Now let’s use the Gin Framework to make that into a web application.

package main
import “github.com/gin-gonic/gin”
func main() {
r := gin.Default()
r.GET(“/”, func(c *gin.Context) {
c.String(200, “Hello World”)
})
r.Run()
}

Save and run it

$ go run main.go
[GIN-debug] [WARNING] Running in “debug” mode. Switch to “release” mode in production.
— using env: export GIN_MODE=release
— using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET / → main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2016/12/02–14:57:52 | 200 | 33.798µs | ::1 | GET /

Then browse over to http://localhost:8080


Hello World

Success!!!

We’re building an API though not a web app so let’s switch that to a JSON response

package main
import “github.com/gin-gonic/gin”
func main() {
r := gin.Default()
r.GET(“/”, func(c *gin.Context) {
c.JSON(200, gin.H{
“message”: “Hello World”,
})

})
r.Run() // listen and server on 0.0.0.0:8080
}

Save file, rerun your server and refresh your browser, you should see our message as JSON
{“message”: “Hello World”}

Data Persistence with GORM

Now let’s take a look at our persistence layer. For this section we’re going to start with a local SQLite file based Database you can use locally. Later we’ll switch this to use a MySql to demonstrate that as well.

Gorm http://jinzhu.me/gorm/ is a Object-relational mapping (ORM) framework for go. It significantly simplifies the mapping and persistence of models to the database. While I’m not a huge fan of ORMs for large complex systems, they do work well for prototyping new greenfield applications. Gorm is a popular tool for this in the Go space and we’ll take a look at it here.

To walk through gorm we’re going to swap out the Gin code we just wrote, and demonstrate gorm capabilities for a bit. Once we’ve walked through the key elements we’ll add Gin back into the app.

Let’s get started with a small example.

package main
import (
“github.com/jinzhu/gorm”
_ “github.com/jinzhu/gorm/dialects/sqlite”
)
func main() {
db, _ := gorm.Open(“sqlite3”, “./gorm.db”)
defer db.Close()
}

If you run this right now you’ll see a new file show up in your file system called gorm.db. This is our database file the system will use in the app. While we can see that our app runs, and gorm is getting used, our system isn’t doing much yet. Lets add some more code.

package main
import (
“fmt”
“github.com/jinzhu/gorm”
_ “github.com/jinzhu/gorm/dialects/sqlite”
)
type Person struct {
ID uint `json:”id”`
FirstName string `json:”firstname”`
LastName string `json:”lastname”`
}
func main() {
db, _ := gorm.Open(“sqlite3”, “./gorm.db”)
defer db.Close()
 p1 := Person{FirstName: “John”, LastName: “Doe”}
p2 := Person{FirstName: “Jane”, LastName: “Smith”}
 fmt.Println(p1.FirstName)
fmt.Println(p2.LastName)
}

Here we’ve just added a simple Person struct and created a couple instances before using them to print out the values. Remember the fields on the Person struct need to start with a capital letter, as this is how Go understands these are public fields.

Now that we’ve got an object to work with let’s use Gorm.

package main
import (
“fmt”
“github.com/jinzhu/gorm”
_ “github.com/jinzhu/gorm/dialects/sqlite”
)
type Person struct {
ID uint `json:”id”`
FirstName string `json:”firstname”`
LastName string `json:”lastname”`
}
func main() {
db, _ := gorm.Open(“sqlite3”, “./gorm.db”)
defer db.Close()
 db.AutoMigrate(&Person{})
 p1 := Person{FirstName: “John”, LastName: “Doe”}
p2 := Person{FirstName: “Jane”, LastName: “Smith”}
 db.Create(&p1)
var p3 Person // identify a Person type for us to store the results in
db.First(&p3) // Find the first record in the Database and store it in p3
 fmt.Println(p1.FirstName)
fmt.Println(p2.LastName)
fmt.Println(p3.LastName) // print out our record from the database
}

Great now let’s run it and see what we’ve got


$ go run main.go
John
Smith
Doe

Wow that was pretty easy. Just a few lines of code and we’re able to save and retrieve from a database. Gorm has a lot more options on how to store, and query on their site. We’ll get into a couple core pieces next, but do check out their docs for more options

Making the API

We’ve reviewed how the frameworks function independently. Now it’s time to pull everything together into a usable API

Read All

Let’s start with the Read portion of CRUD, by querying the data we added earlier. I’m going to remove a few of the lines we just went through and add back in the Gin framework with a new route to query our DB.

package main
import (
“fmt”
 “github.com/gin-gonic/gin”
“github.com/jinzhu/gorm”
_ “github.com/jinzhu/gorm/dialects/sqlite”
)
var db *gorm.DB
var err error
type Person struct {
ID uint `json:”id”`
FirstName string `json:”firstname”`
LastName string `json:”lastname”`
}
func main() {
 // NOTE: See we’re using = to assign the global var
// instead of := which would assign it only in this function
db, err = gorm.Open(“sqlite3”, “./gorm.db”)
if err != nil {
fmt.Println(err)
}
defer db.Close()
 db.AutoMigrate(&Person{})
 r := gin.Default()
r.GET(“/”, GetProjects)
 r.Run(“:8080”)
}
func GetProjects(c *gin.Context) {
var people []Person
if err := db.Find(&people).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, people)
}
}

Now run it and head to http://localhost:8080 and you should see

[{“id”: 1,”firstname”: “John”,”lastname”: “Doe”}]

Wow just a few lines of code and we’re already getting API responses. Most of that was error handling too!

Read One

OK lets update the context to be more REST oriented and add in the ability to look up a single person.

package main
import (
“fmt”
 “github.com/gin-gonic/gin”
“github.com/jinzhu/gorm”
_ “github.com/jinzhu/gorm/dialects/sqlite”
)
var db *gorm.DB
var err error
type Person struct {
ID uint `json:”id”`
FirstName string `json:”firstname”`
LastName string `json:”lastname”`
}
func main() {
 // NOTE: See we’re using = to assign the global var
// instead of := which would assign it only in this function
db, err = gorm.Open(“sqlite3”, “./gorm.db”)
if err != nil {
fmt.Println(err)
}
defer db.Close()
 db.AutoMigrate(&Person{})
 r := gin.Default()
r.GET(“/people/”, GetPeople)
r.GET(“/people/:id”, GetPerson)
 r.Run(“:8080”)
}
func GetPerson(c *gin.Context) {
id := c.Params.ByName(“id”)
var person Person
if err := db.Where(“id = ?”, id).First(&person).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, person)
}
}
func GetPeople(c *gin.Context) {
var people []Person
if err := db.Find(&people).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, people)
}
}

Now run the server but NOTE that we changed the context so now you’ll head over to http://localhost:8080/people/ to see your list. Once there add the ID to the end of the url and you’ll get the single record back http://localhost:8080/people/1

{“id”: 1,”firstname”: “John”,”lastname”: “Doe”}

Create

That was hard to see the difference using only one record. It was hard to tell the difference between [{…}] and {…} So let’s add in the Create function and route

package main
import (
“fmt”
 “github.com/gin-gonic/gin”
“github.com/jinzhu/gorm”
_ “github.com/jinzhu/gorm/dialects/sqlite”
)
var db *gorm.DB
var err error
type Person struct {
ID uint `json:”id”`
FirstName string `json:”firstname”`
LastName string `json:”lastname”`
}
func main() {
 // NOTE: See we’re using = to assign the global var
// instead of := which would assign it only in this function
db, err = gorm.Open(“sqlite3”, “./gorm.db”)
if err != nil {
fmt.Println(err)
}
defer db.Close()
db.AutoMigrate(&Person{})
 r := gin.Default()
r.GET(“/people/”, GetPeople)
r.GET(“/people/:id”, GetPerson)
r.POST(“/people”, CreatePerson)
r.Run(“:8080”)
}
func CreatePerson(c *gin.Context) {
 var person Person
c.BindJSON(&person)
 db.Create(&person)
c.JSON(200, person)
}
func GetPerson(c *gin.Context) {
id := c.Params.ByName(“id”)
var person Person
if err := db.Where(“id = ?”, id).First(&person).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, person)
}
}
func GetPeople(c *gin.Context) {
var people []Person
if err := db.Find(&people).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, people)
}
}

Now to test this one we’re going to run a curl command from the command line. We’ll also need the server running so open up another terminal window so you can run both commands. Run your server in the first window with $ go run main.go

Once it’s running, in the second window run:

$ curl -i -X POST http://localhost:8080/people -d ‘{ “FirstName”: “Elvis”, “LastName”: “Presley”}’

You should see a successful response

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 03 Dec 2016 00:14:06 GMT
Content-Length: 50
{“id”:2,”firstname”:”Elvis”,”lastname”:”Presley”}

Now lets list our people in the browser to see that it lists all our entries
http://localhost:8080/people/

[{“id”: 1,”firstname”: “John”,”lastname”: “Doe”},{“id”: 2,”firstname”: “Elvis”,”lastname”: “Presley”}]

Fantastic, it worked! You think that’s cool watch this.

This time only send in part of the Person

$ curl -i -X POST http://localhost:8080/people -d ‘{ “FirstName”: “Madison”}’

Refresh your browser and see that it added only the data we sent in

[{“id”: 1,”firstname”: “John”,”lastname”: “Doe”},{“id”: 2,”firstname”: “Elvis”,”lastname”: “Presley”},{“id”: 3,”firstname”: “Madison”,”lastname”: “”}]

That’s part of Gin, note the c.BindJSON(&person) line in the CreatePerson function. It automatically populates any matching data fields from the request.

Also you may have missed it but the case in my database and the case i passed in is different. Gin is very forgiving with the case of the fields as well. I passed in FirstName but the DB uses firstname.

So simple!

Update

We can’t leave Madison with no last name though. Time to add our update function

package main
import (
“fmt”
 “github.com/gin-gonic/gin”
“github.com/jinzhu/gorm”
_ “github.com/jinzhu/gorm/dialects/sqlite”
)
var db *gorm.DB
var err error
type Person struct {
ID uint `json:”id”`
FirstName string `json:”firstname”`
LastName string `json:”lastname”`
}
func main() {
 // NOTE: See we’re using = to assign the global var
// instead of := which would assign it only in this function
db, err = gorm.Open(“sqlite3”, “./gorm.db”)
if err != nil {
fmt.Println(err)
}
defer db.Close()
 db.AutoMigrate(&Person{})
 r := gin.Default()
r.GET(“/people/”, GetPeople)
r.GET(“/people/:id”, GetPerson)
r.POST(“/people”, CreatePerson)
r.PUT(“/people/:id”, UpdatePerson)
 r.Run(“:8080”)
}
func UpdatePerson(c *gin.Context) {
 var person Person
id := c.Params.ByName(“id”)
 if err := db.Where(“id = ?”, id).First(&person).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
}
c.BindJSON(&person)
 db.Save(&person)
c.JSON(200, person)
}
func CreatePerson(c *gin.Context) {
 var person Person
c.BindJSON(&person)
 db.Create(&person)
c.JSON(200, person)
}
func GetPerson(c *gin.Context) {
id := c.Params.ByName(“id”)
var person Person
if err := db.Where(“id = ?”, id).First(&person).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, person)
}
}
func GetPeople(c *gin.Context) {
var people []Person
if err := db.Find(&people).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, people)
}
}

To test this one, we’ll use a similar curl command but we’ll call PUT method on a specific user

$ curl -i -X PUT http://localhost:8080/people/3 -d ‘{ “FirstName”: “Madison”, “LastName”:”Sawyer” }’
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 03 Dec 2016 00:25:35 GMT
Content-Length: 51
{“id”:3,”firstname”:”Madison”,”lastname”:”Sawyer”}

Sure enough if we refresh our browser we see it added sawyer to the last name.

[{“id”: 1,”firstname”: “John”,”lastname”: “Doe”},{“id”: 2,”firstname”: “Elvis”,”lastname”: “Presley”},{“id”: 3,”firstname”: “Madison”,”lastname”: “Sawyer”}]

Again we can send in partial data for a partial update

$ curl -i -X PUT http://localhost:8080/people/3 -d ‘{ “FirstName”: “Tom” }’

Which shows up as

[{“id”: 1,”firstname”: “John”,”lastname”: “Doe”},{“id”: 2,”firstname”: “Elvis”,”lastname”: “Presley”},{“id”: 3,”firstname”: “Tom”,”lastname”: “Sawyer”}]

Delete

Now to round out the solution lets dd in the delete function.

package main
import (
“fmt”
 “github.com/gin-gonic/gin”
“github.com/jinzhu/gorm”
_ “github.com/jinzhu/gorm/dialects/sqlite”
)
var db *gorm.DB
var err error
type Person struct {
ID uint `json:”id”`
FirstName string `json:”firstname”`
LastName string `json:”lastname”`
}
func main() {
 // NOTE: See we’re using = to assign the global var
// instead of := which would assign it only in this function
db, err = gorm.Open(“sqlite3”, “./gorm.db”)
if err != nil {
fmt.Println(err)
}
defer db.Close()
 db.AutoMigrate(&Person{})
 r := gin.Default()
r.GET(“/people/”, GetPeople)
r.GET(“/people/:id”, GetPerson)
r.POST(“/people”, CreatePerson)
r.PUT(“/people/:id”, UpdatePerson)
r.DELETE(“/people/:id”, DeletePerson)
 r.Run(“:8080”)
}
func DeletePerson(c *gin.Context) {
id := c.Params.ByName(“id”)
var person Person
d := db.Where(“id = ?”, id).Delete(&person)
fmt.Println(d)
c.JSON(200, gin.H{“id #” + id: “deleted”})
}
func UpdatePerson(c *gin.Context) {
 var person Person
id := c.Params.ByName(“id”)
 if err := db.Where(“id = ?”, id).First(&person).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
}
c.BindJSON(&person)
 db.Save(&person)
c.JSON(200, person)
}
func CreatePerson(c *gin.Context) {
 var person Person
c.BindJSON(&person)
 db.Create(&person)
c.JSON(200, person)
}
func GetPerson(c *gin.Context) {
id := c.Params.ByName(“id”)
var person Person
if err := db.Where(“id = ?”, id).First(&person).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, person)
}
}
func GetPeople(c *gin.Context) {
var people []Person
if err := db.Find(&people).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, people)
}
}

And to test this we’ll call it using the Delete method with curl

$ curl -i -X DELETE http://localhost:8080/people/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 03 Dec 2016 00:32:40 GMT
Content-Length: 20
{“id #1”:”deleted”}

Refresh your browser and you’ll see our John Doe has been removed

[{“id”: 2,”firstname”: “Elvis”,”lastname”: “Presley”},{“id”: 3,”firstname”: “Tom”,”lastname”: “Sawyer”}]

Changing the Model

With the basic API defined, now is a great time to start changing the Person object. We can easily modify the database and api by only changing the person struct.

All i’m going to do is add a city field to the Person Struct. Nothing else. One line.

type Person struct {
ID uint `json:”id”`
FirstName string `json:”firstname”`
LastName string `json:”lastname”`
City string `json:”city”`
}

Refreshing our browser and pulling a list you can see all my objects now have city

[{“id”: 2,”firstname”: “Elvis”,”lastname”: “Presley”,”city”: “”},{“id”: 3,”firstname”: “Tom”,”lastname”: “Sawyer”,”city”: “”}]

I can create and update the new field with no other changes

$ curl -i -X PUT http://localhost:8080/people/2 -d ‘{ “city”: “Memphis” }’
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 03 Dec 2016 00:40:57 GMT
Content-Length: 67
{“id”:2,”firstname”:”Elvis”,”lastname”:”Presley”,”city”:”Memphis”}

This is all handled by the db.AutoMigrate(&Person{}) line in our main function. In a production environment we’d want to manage the schema a lot closer, but for prototyping this is perfect.

Using MySql

I hear ya. This is great but what about using MySql instead of SQLite.

For that we just need to swap out an import statement and the connection..

Import _ “github.com/go-sql-driver/mysql”

Connection

db, _ = gorm.Open(“mysql”, “user:pass@tcp(127.0.0.1:3306)/samples?charset=utf8&parseTime=True&loc=Local”)

Full example

package main
// only need mysql OR sqlite 
// both are included here for reference
import (
“fmt”
 “github.com/gin-gonic/gin”
_ “github.com/go-sql-driver/mysql”
“github.com/jinzhu/gorm”
_ “github.com/jinzhu/gorm/dialects/sqlite”
)
var db *gorm.DB
var err error
type Person struct {
ID uint `json:”id”`
FirstName string `json:”firstname”`
LastName string `json:”lastname”`
City string `json:”city”`
}
func main() {
 // NOTE: See we’re using = to assign the global var
// instead of := which would assign it only in this function
//db, err = gorm.Open(“sqlite3”, “./gorm.db”)
db, _ = gorm.Open(“mysql”, “user:pass@tcp(127.0.0.1:3306)/database?charset=utf8&parseTime=True&loc=Local”)
 if err != nil {
fmt.Println(err)
}
defer db.Close()
 db.AutoMigrate(&Person{})
 r := gin.Default()
r.GET(“/people/”, GetPeople)
r.GET(“/people/:id”, GetPerson)
r.POST(“/people”, CreatePerson)
r.PUT(“/people/:id”, UpdatePerson)
r.DELETE(“/people/:id”, DeletePerson)
 r.Run(“:8080”)
}
func DeletePerson(c *gin.Context) {
id := c.Params.ByName(“id”)
var person Person
d := db.Where(“id = ?”, id).Delete(&person)
fmt.Println(d)
c.JSON(200, gin.H{“id #” + id: “deleted”})
}
func UpdatePerson(c *gin.Context) {
 var person Person
id := c.Params.ByName(“id”)
 if err := db.Where(“id = ?”, id).First(&person).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
}
c.BindJSON(&person)
 db.Save(&person)
c.JSON(200, person)
}
func CreatePerson(c *gin.Context) {
 var person Person
c.BindJSON(&person)
 db.Create(&person)
c.JSON(200, person)
}
func GetPerson(c *gin.Context) {
id := c.Params.ByName(“id”)
var person Person
if err := db.Where(“id = ?”, id).First(&person).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, person)
}
}
func GetPeople(c *gin.Context) {
var people []Person
if err := db.Find(&people).Error; err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, people)
}
}

Conclusion

Go is a flexible language with a robust environment. Its extremely easy to build feature rich applications quickly and with a small amount of code. I hope this was a useful walkthrough. Please feel free to comment with your thoughts and questions

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.