📚 Contents
- What are we building
- Go
- Express and Fiber
- Let's Get Started With APIs
- Setup Essentials
- Add the Routes
Let's learn tech in plain english
If you are better with code than with text, feel free to directly jump to completed project, fork it and start adding your code -
percoguru / tutorial-notes-api-go-fiber
Build a RESTful API in Go: feat Fiber, Postgres, GORM
👷 What are we building ↑
We will be building a simple Note Taking API in Go to setup a basic repo which can be used to scale and create a complete backend service. The structure we create will help you understand and get started with APIs in Go.
If you have wondered how to start developing APIs in Go or you just completed understanding basics of Go and want to get started with real development this will be a great starting point.
🚅 Go ↑
Go has been around for more than a decade now. It is super loved and super fun to learn and work with.
Fully fledged full stack web applications can be built using Go, but for the purpose of this article which is to learn to build a basic web API in GoLang, we will stick to APIs.
⏩ Express and Fiber ↑
NodeJs has enjoyed a lot of love from people building backend services for the past decade.
Developing APIs the Express way is super developer friendly for both polished NodeJs developers and new Go Developers. Thus Fiber was built with Express in mind.
Fiber is a web framework for Go with APIs readiness from scratch, middleware support and super fast performance. We will be making use of Fiber to create our web API.
📁 Final Project Structure
Let's first look at what our final structure will look like -
notes-api-fiber
|
- config
| - config.go
|
- database
| - connect.go
| - database.go
|
- internal
| |
| - handler
| | - note
| | - noteHandler.go
| |
| - model
| | - model.go
| |
| - routes
| - note
| - noteRoutes.go
|
- router
| - router.go
|
- main.go
- go.mod
- go.sum
No need to worry, we will start building with just a single file and reach this state with sufficient logic and explanations on the way.
🎒 Basics of packages -
Go code is distributed in packages and we will be making a lot of them. Go packages are used for distribution of code and logic based on their usage. It can also be observed in the directory structure we draw above.
- We declare the package name for a file by writing
package <package_name>
at the top. - In simple words packages are group of code in a go project that share the variables and functions.
- Any number of files part of the same package share variables, functions, types, interfaces, basically any definition.
- Inorder to reach any code for a package correctly all files for a package should be present in a single directory.
💡 Let's Get Started With APIs ↑
We will be beginning with a single file, the starting point of our code - main.go
. Create this file in the root directory of our project.
main.go ↑
Let's start with writing the root file, the main.go
file. This will be the starting point of the application. Right now, we will just be initializing a fiber app inside here. We will add more things later and this will become the place where the setup happens.
main.go
package main
import (
"github.com/gofiber/fiber/v2"
)
func main() {
// Start a new fiber app
app := fiber.New()
// Listen on PORT 300
app.Listen(":3000")
}
Let's create an endpoint ↑
To have a basic understanding of how an API endpoint is created, let's first create a dummy endpoint to get started.
If you have worked with Express you might notice the resemblance, if you have not worked with Express the resemblance will come other way around for you
main.go
package main
import (
"github.com/gofiber/fiber/v2"
)
func main() {
// Start a new fiber app
app := fiber.New()
// Send a string back for GET calls to the endpoint "/"
app.Get("/", func(c *fiber.Ctx) error {
err := c.SendString("And the API is UP!")
return err
})
// Listen on PORT 3000
app.Listen(":3000")
}
Run the server by running
go run main.go
in the root directory. Then go to localhost:3000
. You will see a page like this -
Now, that we have seen that how we can startup an API from a single file, containing a few lines of code.
Note that you can keep on adding more and more Endpoints in here and scale. Such scenario will come up many times during this article, but instead of scaling vertically we will try to distribute our code horizontally wherever possible
Now our project directory looks like -
notes-api-fiber
|
- main.go
💼 Go modules ↑
Our project will become a Go Module. To know more about Go Modules visit https://golang.org/ref/mod. To start a Go module within our project directory run the command
go mod init <your_module_url>
Normally <your_module_url>
is represented by where your module will be published. For now you can use your Github profile. Eg - If your github username is percoguru
, you will run
go mod init github.com/percoguru/notes-api-fiber
The last part of your path is the name of your project.
Now any package that you create will be a subdirectory within this module. Eg a package foo
would be imported by another package inside your module as github.com/percoguru/notes-api-fiber/foo
.
Once you run the command, a file go.mod
will be created that contains the basic information about our module and the dependencies that we would be adding. This can be considered as the package.json
equivalent for Go.
Your go.mod
will look like this -
go.mod
module github.com/percoguru/notes-api-fiber
go 1.17
📐 Setup Essentials ↑
Now we will set up some basic stuff to support our APIs -
- Makefile
- Database (PostgreSQL)
- Models
- Environment Variables
Makefile ↑
As we introduce more and more changes, we will need to run go run main.go
every time we want changes to reflect on our running server. To enable hot reload of our server create a Makefile
in the root of your directory.
- Install reflex
go install github.com/cespare/reflex@latest
- Add commands to your
Makefile
build:
go build -o server main.go
run: build
./server
watch:
reflex -s -r '\.go$$' make run
- Run
make watch
- Make any changes to your code and see the server reloading in the terminal.
💾 Database ↑
We will be using Postgres for the database implementation in this article.
Although we are making just a Note Taking Application here, the purpose of this article is to allow you to scale from here. I would even encourage to go crazy with the database schema now only and add new entities or use something other than Notes. SQL is great for such scaling and you would not be exploring other options if you decide to scale further from here.
Get Postgres running on your machine, and create a database fiber-app for our implementation - follow instructions at - https://www.postgresql.org/download/
⚙️ Adding Config ↑
We will be adding an environment variable file .env
in the root of our project directory. When we connect to a database we will require some variables, we will store those in this file.
.env
DB_HOST= localhost
DB_NAME= fiber-app
DB_USER= postgres
DB_PASSWORD= postgres
DB_PORT= 5432
The above values will most probably remain the same for you too except the password, which will be the password you choose for the postgres user. Remember to create a database fiber-app before going ahead.
Now we have to pick up these variables from the .env
file. For this purpose we will create a package config
that will provide us the configurations for the project.
Create a folder config
in the root of your directory and create a file config.go
inside this.
First run -
go get github.com/joho/godotenv
config.go
package config
import (
"fmt"
"os"
"github.com/joho/godotenv"
)
// Config func to get env value
func Config(key string) string {
// load .env file
err := godotenv.Load(".env")
if err != nil {
fmt.Print("Error loading .env file")
}
// Return the value of the variable
return os.Getenv(key)
}
🔄 Connecting to the Database ↑
Inside the root folder of our project, create a directory named database
. All our code related to database connection and migrations will reside here. This will become a package related to our database connection related operations, let's name this package database
.
We will be using an ORM (Object Relational Mapping) as a middleware between our Go code and SQL database. GORM would be our ORM of choice for this article. It supports Postgres, Associations, Hooks and one feature that will help us a lot initially - Auto Migrations.
Add gorm and postgres driver for gorm by running -
go get gorm.io/gorm
go get gorm.io/driver/postgres
connect.go
package database
import (
"fmt"
"log"
"strconv"
"github.com/percoguru/notes-api-fiber/config"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
// Declare the variable for the database
var DB *gorm.DB
// ConnectDB connect to db
func ConnectDB() {
var err error
p := config.Config("DB_PORT")
port, err := strconv.ParseUint(p, 10, 32)
if err != nil {
log.Println("Idiot")
}
// Connection URL to connect to Postgres Database
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", config.Config("DB_HOST"), port, config.Config("DB_USER"), config.Config("DB_PASSWORD"), config.Config("DB_NAME"))
// Connect to the DB and initialize the DB variable
DB, err = gorm.Open(postgres.Open(dsn))
if err != nil {
panic("failed to connect database")
}
fmt.Println("Connection Opened to Database")
}
Notice how connect.go
imports the package config
. It looks for the package inside the folder ./config
.
We have created the database connector, but when we run the application we run go run main.go
. Right now we have not yet called the function connectDB()
. We need to call this in order to connect to the database.
Let's go to our main.go
file and connect to the database. We want to connect to our database whenever the server is run. So we can call the function connectDB()
from the function main
of the package main.
main.go
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/percoguru/notes-api-fiber/database"
)
func main() {
// Start a new fiber app
app := fiber.New()
// Connect to the Database
database.ConnectDB()
// Send a string back for GET calls to the endpoint "/"
app.Get("/", func(c *fiber.Ctx) error {
err := c.SendString("And the API is UP!")
return err
})
// Listen on PORT 3000
app.Listen(":3000")
}
Now run
go run main.go
And you are connected to the Database. You will see an output like this.
We have not yet done any operations on the database. We will be creating models to represent the tables we want to store in the database.
🔖 Add Models
Create a folder internal
in the root of your directory. All our internal logic (models, types, handlers, routes, constants etc.) will be stored in this folder.
Within this folder create a folder model
and then the file model.go
. Thus creating internal/model/model.go
. The folder model will contain our package model
.
We will be creating only one model for now - note, you can add more here or use something other than a note, like product, page, etc. Be sure to be creative. Even if you choose to go with some other model (which I am encouraging btw 😉) the other parts of code would remain mostly the same, so, don't hesitate to experiment.
Our Notes table would look like -
- ID uuid
- Title text
- SubTitle text
- Text text
To use uuid type in Go, run -
go get github.com/google/uuid
To create this model, add this to the model.go
file -
package model
import (
"github.com/google/uuid"
"gorm.io/gorm"
)
type Note struct {
gorm.Model // Adds some metadata fields to the table
ID uuid.UUID `gorm:"type:uuid"` // Explicitly specify the type to be uuid
Title string
SubTitle string
Text string
}
Notice that in the line
ID uuid.UUID `gorm:"type:uuid"`
We are first telling Go that the type of this struct field is uuid.UUID
and then telling GORM to create this column with type uuid by specifying with the tag gorm:"type:uuid"
🔁 Auto Migrations
GORM supports auto migrations, thus whenever you make changes to your model structs (like add column, change type, add index) and restart the server the changes will be reflected in the database automatically.
Note that to save you from accidental loss of data GORM does not delete columns automatically through migrations if you do so in your struct. Though you can configure GORM to do so.
Go into your database/connect.go
file and add in a line to automigrate after connecting to the database
...
// Connect to the DB and initialize the DB variable
DB, err = gorm.Open(postgres.Open(dsn))
if err != nil {
panic("failed to connect database")
}
fmt.Println("Connection Opened to Database")
// Migrate the database
DB.AutoMigrate(&model.Note{})
fmt.Println("Database Migrated")
}
Now, restart the server. And your database is migrated. You can verify the migration by going into postgres, on Linux you can use psql and on Windows pgAdmin or DBeaver.
On psql on Ubuntu -
- Connect to the database notes-api
\c notes-api
- Get details about the notes table Note that GORM pluralizes the struct name to be the table name, also note in the below image how field names are handled against corresponding field names in struct
\d+ notes
Output
The extra fields have been added by GORM, when we added gorm.Model
at the top of our struct.
Now we have verified our migrations and our model is in the database. 😎
Let's re-look at our project structure now -
notes-api-fiber
|
- config
| - config.go
|
- database
| - connect.go
|
- internal
| |
| |- model
| | - model.go
|
- main.go
- go.mod
- go.sum
- .env
🚡 Add The Routes ↑
Our API will have routes which are endpoints that the browser or a web or mobile application will use to perform CRUD operations on the data.
First we setup a basic router to start routing on the fiber app.
Create a folder router
in the root directory of our project and inside it a file router.go
.
Inside the file add the following code -
package router
import "github.com/gofiber/fiber/v2"
func SetupRoutes(app *fiber.App) {
}
We just declared a function SetupRoutes inside package router that takes a fiber app as an argument. This function will take a fiber app and route calls to this app to specific routes or route handlers.
APIs are grouped based on parameters and fiber allows us to do so. For example if there are three API endpoints -
- GET
api/user/:userId
- Get User with userId - GET
api/user
- Get All Users - PUT
api/user/:userId
- Update User with userId
We do not have to write all the repeated parameters. What we can instead do is -
...
app := fiber.App()
api := app.Group("api")
user := api.Group("user")
user.GET("/", func(c *fiber.Ctx) {} )
user.GET("/:userId", func(c *fiber.Ctx) {} )
user.PUT("/:userId" ,func(c *fiber.Ctx) {} )
This helps a lot when we scale and add a lot of API and complex routing.
Note that 'api' is of type fiber.Router and 'app' is of type fiber.App and these both have the function Group
🔨 Routes ↑
We want to keep our routes like SERVER_HOST/api/param1/param2
. Thus we add this line to our function SetupRoutes
-
api := app.Group("/api", logger.New()) // Group endpoints with param 'api' and log whenever this endpoint is hit.
The handler logger.New()
will also log all the API calls and their statuses.
Now as I had said earlier we would scale horizontally, we could have added routes related to notes in the main.go
file itself, but we created a router package to handle API routes. Now, when the API will scale you will be adding a lot of models so we can not add add notes api in router package as well.
We will add specific router for each model and right now for Notes
Inside the internal
folder create a folder routes
this is where we will have subdirectories for all routes related to a model. Within the routes folder add a folder note
and inside the folder add a file note.go
.
You have created -
internal/routes/note/note.go
Inside the file note.go
add the following code -
package noteRoutes
import "github.com/gofiber/fiber/v2"
func SetupNoteRoutes(router fiber.Router) {
}
The function SetupNoteRoutes takes a fiber.Router and handles endpoints to the note model. Thus add the line -
note := router.Group("/note")
We will be adding CRUD (Create, Read, Update, Delete) operations to the note routes. Thus -
package noteRoutes
import "github.com/gofiber/fiber/v2"
func SetupNoteRoutes(router fiber.Router) {
note := router.Group("/note")
// Create a Note
note.Post("/", func(c *fiber.Ctx) error {})
// Read all Notes
note.Get("/", func(c *fiber.Ctx) error {})
// Read one Note
note.Get("/:noteId", func(c *fiber.Ctx) error {})
// Update one Note
note.Put("/:noteId", func(c *fiber.Ctx) error {})
// Delete one Note
note.Delete("/:noteId", func(c *fiber.Ctx) error {})
}
Note that we have to write handler function to do the tasks we have commented for all the API endpoints
We will be writing these handler in a seperate package
🔧 Handlers ↑
Handlers are functions that take in a Fiber Context (fiber.Ctx) and use the request, send the response or just act as a middleware and pass on the authority to the next handler.
To know more about handlers and middleware visit the Fiber Documentation
Inside the internal
folder add a folder handlers
this will contain all the API handlers with a specific sub directory for each model. So create a folder note
within handlers
and add a file note.go
inside the note
folder.
You just created -
internal/handlers/note/note.go
Now we will add handlers that we require in routes/note/note.go
file to handlers/note/note.go
file.
handlers/note/note.go
package noteHandler
- Add the Read Notes handler -
func GetNotes(c *fiber.Ctx) error {
db := database.DB
var notes []model.Note
// find all notes in the database
db.Find(¬es)
// If no note is present return an error
if len(notes) == 0 {
return c.Status(404).JSON(fiber.Map{"status": "error", "message": "No notes present", "data": nil})
}
// Else return notes
return c.JSON(fiber.Map{"status": "success", "message": "Notes Found", "data": notes})
}
- Add the Create Note handler -
func CreateNotes(c *fiber.Ctx) error {
db := database.DB
note := new(model.Note)
// Store the body in the note and return error if encountered
err := c.BodyParser(note)
if err != nil {
return c.Status(500).JSON(fiber.Map{"status": "error", "message": "Review your input", "data": err})
}
// Add a uuid to the note
note.ID = uuid.New()
// Create the Note and return error if encountered
err = db.Create(¬e).Error
if err != nil {
return c.Status(500).JSON(fiber.Map{"status": "error", "message": "Could not create note", "data": err})
}
// Return the created note
return c.JSON(fiber.Map{"status": "success", "message": "Created Note", "data": note})
}
- Add the Get Note Handler
func GetNote(c *fiber.Ctx) error {
db := database.DB
var note model.Note
// Read the param noteId
id := c.Params("noteId")
// Find the note with the given Id
db.Find(¬e, "id = ?", id)
// If no such note present return an error
if note.ID == uuid.Nil {
return c.Status(404).JSON(fiber.Map{"status": "error", "message": "No note present", "data": nil})
}
// Return the note with the Id
return c.JSON(fiber.Map{"status": "success", "message": "Notes Found", "data": note})
}
- Add the Update Note Handler
func UpdateNote(c *fiber.Ctx) error {
type updateNote struct {
Title string `json:"title"`
SubTitle string `json:"sub_title"`
Text string `json:"Text"`
}
db := database.DB
var note model.Note
// Read the param noteId
id := c.Params("noteId")
// Find the note with the given Id
db.Find(¬e, "id = ?", id)
// If no such note present return an error
if note.ID == uuid.Nil {
return c.Status(404).JSON(fiber.Map{"status": "error", "message": "No note present", "data": nil})
}
// Store the body containing the updated data and return error if encountered
var updateNoteData updateNote
err := c.BodyParser(&updateNoteData)
if err != nil {
return c.Status(500).JSON(fiber.Map{"status": "error", "message": "Review your input", "data": err})
}
// Edit the note
note.Title = updateNoteData.Title
note.SubTitle = updateNoteData.SubTitle
note.Text = updateNoteData.Text
// Save the Changes
db.Save(¬e)
// Return the updated note
return c.JSON(fiber.Map{"status": "success", "message": "Notes Found", "data": note})
}
- Add the Delete Note Handler
func DeleteNote(c *fiber.Ctx) error {
db := database.DB
var note model.Note
// Read the param noteId
id := c.Params("noteId")
// Find the note with the given Id
db.Find(¬e, "id = ?", id)
// If no such note present return an error
if note.ID == uuid.Nil {
return c.Status(404).JSON(fiber.Map{"status": "error", "message": "No note present", "data": nil})
}
// Delete the note and return error if encountered
err := db.Delete(¬e, "id = ?", id).Error
if err != nil {
return c.Status(404).JSON(fiber.Map{"status": "error", "message": "Failed to delete note", "data": nil})
}
// Return success message
return c.JSON(fiber.Map{"status": "success", "message": "Deleted Note"})
}
📨 Connecting handlers to routes ↑
Add the handlers in the note routes, changing the file routes/note/note.go
to -
routes/note/note.go
package noteRoutes
import (
"github.com/gofiber/fiber/v2"
noteHandler "github.com/percoguru/notes-api-fiber/internals/handlers/note"
)
func SetupNoteRoutes(router fiber.Router) {
note := router.Group("/note")
// Create a Note
note.Post("/", noteHandler.CreateNotes)
// Read all Notes
note.Get("/", noteHandler.GetNotes)
// // Read one Note
note.Get("/:noteId", noteHandler.GetNote)
// // Update one Note
note.Put("/:noteId", noteHandler.UpdateNote)
// // Delete one Note
note.Delete("/:noteId", noteHandler.DeleteNote)
}
Notice how noteHandler is imported because of the mismatch between the folder name and the package name, if you want to avoid this, name the folder as noteHandler too
📨 Setup Note Routes ↑
Setup the Note Routes in the router/router.go
file, editing it to -
router/router.go
package router
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
noteRoutes "github.com/percoguru/notes-api-fiber/internals/routes/note"
)
func SetupRoutes(app *fiber.App) {
api := app.Group("/api", logger.New())
// Setup the Node Routes
noteRoutes.SetupNoteRoutes(api)
}
📨 Setup Router ↑
Until now we have created the router and used it to setup note routes, now we need to setup this router from the main function.
Inside the main.go
remove the dummy endpoint we had created and add in the line -
router.setupRoutes(app)
Converting your main.go
into -
main.go
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/percoguru/notes-api-fiber/database"
"github.com/percoguru/notes-api-fiber/router"
)
func main() {
// Start a new fiber app
app := fiber.New()
// Connect to the Database
database.ConnectDB()
// Setup the router
router.SetupRoutes(app)
// Listen on PORT 3000
app.Listen(":3000")
}
et voilà ! 💥 💰
We did it!
Run
make watch
And try the endpoints out on Postman. And see it all working so very fine.
What's Next!!
Now you have built a web API from scratch in Go. You came across the nuances of Go, Fiber, GORM and Postgres. This has been a basic setup and you can grow your api into a full stack web application -
- Add JWT based authentication
- Add more models to your backend and play around with different data types
- Add in a frontend using a UI framework like React or use Go Templates
Top comments (14)
First of all thank you for this amazing tutorial. I am a newbie in Golang language and i must say i am loving the experience so far. However on completion of this tutorial i am getting this error. If anyone could help me decode, i will appreciate.
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x28 pc=0xe56458]
goroutine 7 [running]:
gorm.io/gorm.(*DB).getInstance(0x167e7f70598)
C:/Users/BENSON OPISA/go/pkg/mod/gorm.io/gorm@v1.23.1/gorm.go:363 +0x18
gorm.io/gorm.(*DB).Find(0x167ed27dfe0, {0x12a7180, 0xc00009f4a0}, {0x0, 0x0, 0x0})
C:/Users/BENSON OPISA/go/pkg/mod/gorm.io/gorm@v1.23.1/finisher_api.go:159 +0x45
github.com/bensonopisa/notes-app/internal/handlers/note.GetNotes(0xc000428000)
D:/projects/golang/notes-app/internal/handlers/note/note.go:20 +0x69
github.com/gofiber/fiber/v2.(*App).next(0xc0000023c0, 0xc000428000)
C:/Users/BENSON OPISA/go/pkg/mod/github.com/gofiber/fiber/v2@v2.27.0/router.go:132 +0x1d8
This is thrown on the line db.Find(¬es) on the getNotes Function on notehandler package.
Sorry for the late reply. Seems like you are trying to access an uninitiated variable or nil value. Can you check if you are doing something different?
Great and many thanks for the detail explanation, its really help.
Just curious about folder structure, why not naming it "handlers/note.go" instead of handlers/noteHandlers.go or routes/noteRoutes.go.
Thanks man! Glad you liked it.
If you mean that why we did not use
handlers/noteHandlers.go
. The reason for this would be when we add more handlers, it would becomehandlers/noteHandlers.go
andhandlers/userHandlers.go
. It would be multiple packages inside the same folder which go does not like a lot.I hope I understood your question.
One of the best Fiber tutorial, I would like to know is it possible to use basic auth with gotemplates engine? I can't find inputs for this scenario.
Hi,
I am getting unexpected fault address 0xed9497c2d
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x1 addr=0xed9497c2d pc=0x4f8f6a]
when trying to return from getnotes
return ctx.JSON(fiber.Map{"status": http.StatusOK, "message": "notes found", "data": notes}), when returing null its working, and also when returning a hardcoded struct its working, but its breaking when returning the data returned from db, any idea why..
Never mind, seems like an issue in the latest version of fiber, can be fixed by
app := fiber.New(fiber.Config{
JSONEncoder: json.Marshal,
JSONDecoder: json.Unmarshal,
})
nice info, good tutorial..
First I saw basic tutorial at youtube.com/playlist?list=PLs1BaES... and then i implemented code using this blog. @percoguru you made my day. Thank you So much.. keep writing
Wow! This is such a well written tutorial! I’ve been writing Express CRUD apps for years, but I’ve just started my Go journey. So happy to have stumbled across this gem! Keep up the great work!
Thanks a lot!!
Thank you! Nice tutorial
Thank you :)