Building an Events Application: Kickstarting Development with Echo and GORM in Go
In my previous blog post “Choosing the right tools,” I decided to develop the events application using the Echo web framework for Golang and GORM for database querying. In this post, we will begin developing the API for the events application. But first, let's quickly review some of my design choices.
The Model
I want to expose a simple API that tracks events - public and private - as well as users and the events they have created and/or are attending. Simple as that. I do, however, want to start off by focusing on the events part, so let’s take a look at what I want an “event” to entail:
Title
Description
A banner/image
Start datetime
End datetime
Address
Geolocation for distance calculation purposes
Pretty simple.
We need to be able to hold a little more information about the event though.
The event should have one or more owners
Users should be able to subscribe to an event - Both as attending and non-attending
For search and filtering purposes the owner of an event should be able to add tags
The owner should be able to make the event private, making it invisible to the public
The owner should be able to disable the event - effectively cancelling it
With GORM you define a database table as a go struct. Representing the table would therefore look something like this:
package model
import (
"time"
"github.com/codingsince1985/geo-golang"
"github.com/lib/pq"
)
type Event struct {
Model
Title string `gorm:"not null;index:title_idx,type:GIN"`
Description string `gorm:"default:NULL"`
Banner_url string `gorm:"default:NULL"`
Owners []User `gorm:"many2many:event_owners"`
Is_private_event bool `gorm:"default:false"`
Time_start time.Time `gorm:"required"`
Time_end time.Time `gorm:"default:NULL;check:time_end > time_start"`
Address *geo.Address `gorm:"embedded"`
Geolocation *geo.Location `gorm:"embedded"`
Tags pq.StringArray `gorm:"type:text[];index:tags_idx,type:GIN"`
Is_enabled bool `gorm:"default:true"`
Subscribers []User `gorm:"many2many:event_subscribers"`
}
Notice the tags for each field in the struct, which start with "gorm:". GORM’s migrate functionality interprets these tags and applies them as constraints on the corresponding database columns when creating the table. This allows us to define a database table using just Go code. Pay attention to the Address and Geolocation fields, as well as the Tags field. The Address and Geolocation types refer to structs from the imported package geo-golang, a geocode package. The Address struct includes fields like "Street" and "HouseNumber", and by using the gorm tag "embedded", we instruct GORM to include all fields in the struct as individual columns in the database table. The Location struct contains fields for latitude and longitude.
One downside of using embedded structs is the inability to apply database constraints or other tags to individual fields, which can make them challenging to work with. In such cases, you might prefer to define the structs yourself instead of importing them from external packages.
The Tags field is a little different. I need to be able to hold several tags across several events and still be able to query them fast. I chose to use a StringArray with GIN indexing, a choice I will elaborate on in a future post about searching and filtering events and writing the appropriate queries.
The Subscribers field is defined as a many-to-many relation with the though-table being called “event_subscribers”. This is a simple solution, and I want to define my own table in the future, so it can hold information such as attending/not attending/maybe attending, and whether the user wants to receive updates about the event they subscribed to. For now, we will define it this way.
You might have noticed the Model field in the beginning of the Event struct. The struct is inheriting from the Model struct, which I have defined in a different file in the same package. The struct holds a unique ID for the row as well as some metadata. GORM has it’s own model struct which you would normally inherit, but it has it’s limitations and, as discussed earlier, I cannot use struct tags on those fields as it is a struct imported from an external package. The gorm.Model looks as such:
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
I do not wish to use unsigned integers as the ID for the database tables, as UUID enables you to uniquely identifying any row in any table. It also makes it near impossible to guess an ID. Therefore I have defined my own base model:
package model
import (
"database/sql"
"time"
"github.com/google/uuid"
)
type Model struct {
ID uuid.UUID `gorm:"<-:create;type:uuid;primary_key;default:uuid_generate_v4()" param:"id" validate:"omitempty,uuid4"`
CreatedAt time.Time `gorm:"autoCreateTime:milli"`
UpdatedAt time.Time `gorm:"autoUpdateTime:milli"`
DeletedAt sql.NullTime `gorm:"index"`
}
Here I make sure a new row always has a UUID generated when first created, as well as other tags for validation and linking it to the request parameter “id”. I also made sure to explicitly define how I want time to be stored in the rows. Specifically in milliseconds. I am going to go into more detail about other struct tags than gorm in a later post.
Creating the endpoints
Now let’s have a look at how to expose an api as simply as possible using Echo.
func main() {
//Connect to db and return a db object
db, err := dbmodule.Connect()
if err != nil {
panic(err)
}
//Migrate the registered structs
dbmodule.Migrate(db)
//Create an instance of echo
e := echo.New()
//Define routing
v1 := e.Group("/v1")
event := v1.Group("/event")
event.POST("/", func(c echo.Context) error {
//Create event logic
})
event.GET("/", func(c echo.Context) error {
//Get event logic
})
event.GET("/:id", func(c echo.Context) error {
//Get event by id logic
})
event.PUT("/:id", func(c echo.Context) error {
//Update event logic
})
event.POST("/:id/image", func(c echo.Context) error {
//Upload image to event logic
})
event.DELETE("/:id", func(c echo.Context) error {
//Delete event logic
})
e.Logger.Fatal(e.Start(":1323"))
}
When run, this should be enough to create an Echo server which listens on port 1323. I have boiled it down quite a bit from my actual implementation. As you can see this does not show how I handle the database connection nor the migration. I doesn’t show my implementation of each endpoint, and I register the routes differently. However, this should be enough to get the ball rolling.
Final thoughts
We have looked at how we can use Go structs with tags to represent tables with GORM. We have also had a brief look at how we can expose an api very quickly, using Echo framework. Some of the code shown above has been simplified to keep the initial definition of the system as simple as possible. For instance, the Event struct in my actual implementation includes more tags for validation and binding purposes. I have also implemented automatic TLS certification and request validation, among other features.
You can access my code and review it in detail on my GitHub repo.