In this tutorial, we will build a REST API in Go using the Gin framework. We will create a simple event app where users can sign up, log in, create events, delete events, edit events and attend events. We will use JWT authentication, authorization to protect routes, middleware, SQL, migrations, and Swagger documentation.
Table of contents
Setting up the project
- Enable live reload:install airhttps://github.com/air-verse/airIf you are using zsh, you can add the following to your
.zshrcfile:export PATH=$PATH:$HOME/go/binCreate a.air.tomlfile in the root of the project. With the following content:root = "." testdata_dir = "testdata" tmp_dir = "tmp" [build] args_bin = [] bin = "./tmp/main" cmd = "go build -o ./tmp/main ./cmd/api" delay = 1000 exclude_dir = ["assets", "tmp", "vendor", "testdata"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false follow_symlink = false full_bin = "" include_dir = [] include_ext = ["go", "tpl", "tmpl", "html"] include_file = [] kill_delay = "0s" log = "build-errors.log" poll = false poll_interval = 0 post_cmd = [] pre_cmd = [] rerun = false rerun_delay = 500 send_interrupt = false stop_on_error = false [color] app = "" build = "yellow" main = "magenta" runner = "green" watcher = "cyan" [log] main_only = false silent = false time = false [misc] clean_on_exit = false [proxy] app_port = 0 enabled = false proxy_port = 0 [screen] clear_on_rebuild = false keep_scroll = true
we will now be able to live reload the application with air.
- Initialize a new Go module:
go mod init rest-api-in-gin- Project Structure Setup:
- Create a
cmddirectory at the root of your project. Insidecmd, add anapidirectory and place amain.gofile within it. - At the root level, create an
internaldirectory. Withininternal, add adatabasedirectory. - Within the
internaldirectory, create aenvdirectory. - Within the
cmddirectory, create amigratedirectory. Insidemigrate, add amain.gofile and amigrationsdirectory.
Your project structure should look like this:
rest-api-in-gin
├── cmd
│ ├── api
│ │ ├── main.go
│ ├── migrate
│ │ ├── main.go
│ │ └── migrations
├── internal
│ ├── database
│ ├── envDatabase Tables Overview
Here is an overview of the tables we will be creating:
Users Table
| Column | Description |
|---|---|
| id | Primary key, auto-incremented, unique identifier for each user. |
| Unique and required, email address of the user. | |
| name | Required, the full name of the user. |
| password | Required, should be stored securely, used for user authentication. |
Events Table
| Column | Description |
|---|---|
| id | Primary key, auto-incremented, unique identifier for each event. |
| owner_id | Foreign key referencing users, links an event to a user. |
| name | Required, the name of the event. |
| description | Required, a brief description of the event. |
| date | Required, the date when the event is scheduled to occur. |
| location | Required, the venue or place where the event will take place. |
Attendees Table
| Column | Description |
|---|---|
| id | Primary key, auto-incremented, unique identifier for each attendee record. |
| user_id | Foreign key referencing users, links an attendee to a user. |
| event_id | Foreign key referencing events, links an attendee to an event. |
The attendees table links users to events, ensuring that each user and event exists. If a user or event is deleted, related attendee records are also removed automatically.
Migrations
This project uses golang-migrate for database migrations. First, install the migrate CLI:
Golang migrate https://github.com/golang-migrate/migrate/blob/master/cmd/migrate/README.md
Add the following code to the cmd/migrate/main.go file:
package main
import (
"database/sql"
"log"
"os"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source/file"
)
func main() {
if len(os.Args) < 2 {
log.Fatal("Please provide a migration direction: 'up' or 'down'")
}
direction := os.Args[1]
db, err := sql.Open("sqlite3", "./data.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
instance, err := sqlite3.WithInstance(db, &sqlite3.Config{})
if err != nil {
log.Fatal(err)
}
fSrc, err := (&file.File{}).Open("cmd/migrate/migrations")
if err != nil {
log.Fatal(err)
}
m, err := migrate.NewWithInstance("file", fSrc, "sqlite3", instance)
if err != nil {
log.Fatal(err)
}
switch direction {
case "up":
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
log.Fatal(err)
}
case "down":
if err := m.Down(); err != nil && err != migrate.ErrNoChange {
log.Fatal(err)
}
default:
log.Fatal("Invalid direction. Use 'up' or 'down'.")
}
}Here is a breakdown of the migration code:
- We Check for a migration direction (
upordown) from command-line arguments. - Connect to the SQLite database (
data.db). - Create a migration instance using the database connection and migration files.
- If the direction is "up," apply all pending migrations to update the schema.
- If the direction is "down," roll back the most recent migration.
- Log errors for invalid directions or failed migration operations, ignoring
ErrNoChange.
Lets create our migrations by running the following commands:
migrate create -ext sql -dir ./cmd/migrate/migrations -seq create_users_table
migrate create -ext sql -dir ./cmd/migrate/migrations -seq create_events_table
migrate create -ext sql -dir ./cmd/migrate/migrations -seq create_attendees_tableThis will create 6 files in the cmd/migrate/migrations folder. one up and one down for each migration.
Open up the 000001_create_users_table.up.sql file and add the following code to the file:
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
password TEXT NOT NULL
);Next open up the 000002_create_events_table.up.sql file and add the following code to the file:
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_id INTEGER NOT NULL,
name TEXT NOT NULL,
description TEXT NOT NULL,
date DATETIME NOT NULL,
location TEXT NOT NULL,
FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE
);An event has an owner id that references the user id, this will be used to restrict the events that a user can delete and update. If a user is deleted, all events created by that user will also be deleted.
The last migration file is the 000003_create_attendees_table.up.sql file. Open it up and add the following code to the file:
CREATE TABLE IF NOT EXISTS attendees (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
event_id INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (event_id) REFERENCES events (id) ON DELETE CASCADE
);The attendees table links users to events, ensuring that each user and event exists. If a user or event is deleted, related attendee records are also removed automatically.
To every down file we need to add the following code:
-- 000001_create_users_table.down.sql
DROP TABLE IF EXISTS users;-- 000002_create_events_table.down.sql
DROP TABLE IF EXISTS events;-- 000003_create_attendees_table.down.sql
DROP TABLE IF EXISTS attendees;We can now run the migrations by running the following command:
go run ./cmd/migrate/main.go upThis should now created a data.db file in the root of the project. We can view the database in a GUI like TablePlus. It would look something like this:

Connecting our api app to the database
Open up the main.go file in the cmd/api folder and add the following code to the file:
package main
import (
"database/sql"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./data.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
}We open a connection to the database and check for errors.
We then use the defer keyword to close the database connection when the main function exits.
Models
We will be creating 3 models:
UserEventAttendee
Start by creating a models.go file in the internal/database folder.
With the following code:
package database
import "database/sql"
type Models struct {
Users UserModel
Events EventModel
Attendees AttendeeModel
}
func NewModels(db *sql.DB) Models {
return Models{
Users: UserModel{DB: db},
Events: EventModel{DB: db},
Attendees: AttendeeModel{DB: db},
}
}Here we are creating a Models struct with 3 fields: Users, Events, and Attendees.
We are also creating a NewModels function that takes a *sql.DB instance as an argument and passes it to the UserModel, EventModel, and AttendeeModel structs.
Next we will create the UserModel struct. Create a users.go file in the internal/database folder.
package database
import "database/sql"
type UserModel struct {
DB *sql.DB
}
type User struct {
Id int `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Password string `json:"-"`
}- The
UserModelstruct contains aDBfield, which is a pointer to asql.DBinstance. - The
Userstruct includes four fields:Id,Email,Password, andName. jsontags are used to define how the struct fields are converted to and from JSON, ensuring proper data serialization and deserialization.- The
Passwordfield is marked with a-in thejsontag, instructing the JSON package to exclude it from JSON responses, making sure we don't expose the password in the response.
The next model we will create is the EventModel struct. Create a events.go file in the internal/database folder.
package database
import "database/sql"
type EventModel struct {
DB *sql.DB
}
type Event struct {
Id int `json:"id"`
OwnerId int `json:"ownerId" binding:"required"`
Name string `json:"name" binding:"required,min=3"`
Description string `json:"description" binding:"required,min=10"`
Date string `json:"date" binding:"required,datetime=2006-01-02"`
Location string `json:"location" binding:"required,min=3"`
}- The
Eventstruct includes five fields:Id,OwnerId,Name,Description,Date, andLocation. - We set binding tags and some validation rules. These will used later when creating an event and binding the request body to the
Eventstruct. This is done by the Gin framework. - For now set a binding tag on the
OwnerIdfield. Later we will remove it and instead use the current logged in user.
After that we will create the AttendeeModel struct. Create a attendees.go file in the internal/database folder.
package database
import "database/sql"
type AttendeeModel struct {
DB *sql.DB
}
type Attendee struct {
Id int `json:"id"`
UserId int `json:"userId"`
EventId int `json:"eventId"`
}- The
Attendeestruct includes three fields:Id,UserId, andEventId. - An attendee is a user that has signed up for an event. An event can have many attendees and an attendee can attend many events.
Setting Up the Gin Server
- Create a
routes.gofile in thecmd/apifolder.This file will define the routes for your Gin server.package main import ( "net/http" "github.com/gin-gonic/gin" ) func (app *application) routes() http.Handler { g := gin.Default() return g }We create a functionroutesthat initializes a new Gin server instance usinggin.Default(), which sets up some default middleware (like logging and recovery). Currently, it just returns the Gin instance. We will add some routes to this instance later. - Create a
server.gofile in thecmd/apifolder.This file will handle starting the HTTP server.package main import ( "fmt" "log" "net/http" "time" ) func serve(app *application) error { server := &http.Server{ Addr: fmt.Sprintf(":%d", app.port), Handler: app.routes(), IdleTimeout: time.Minute, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, } log.Printf("Starting server on port %d", app.port) return server.ListenAndServe() }Theservefunction sets up an HTTP server with specific configurations like address, handler, and timeouts. It uses theroutesfunction to get the handler (Gin instance) for the server. The server is started withListenAndServe, if there is an error it will log the error and exit the program. - Create a
env.gofile in theinternal/envfolder.Add the following code to theenv.gofile:package env import ( "os" "strconv" ) func GetEnvString(key, defaultValue string) string { if value, exists := os.LookupEnv(key); exists { return value } return defaultValue } func GetEnvInt(key string, defaultValue int) int { if value, exists := os.LookupEnv(key); exists { if intValue, err := strconv.Atoi(value); err == nil { return intValue } } return defaultValue }TheGetEnvStringandGetEnvIntfunctions are used to get the value of an environment variable. If the environment variable is not set, the function returns the default value. - Put It All Together in the
main.goin thecmd/apifolder:package main import ( "database/sql" "log" "rest-api-in-gin/internal/database" "rest-api-in-gin/internal/env" – "github.com/mattn/go-sqlite3" _ "github.com/joho/godotenv/autoload" // Automatically loads environment variables ) type application struct { port int jwtSecret string models database.Models } func main() { db, err := sql.Open("sqlite3", "./data.db") if err != nil { log.Fatal(err) } defer db.Close() models := database.NewModels(db) app := &application{ port: env.GetEnvInt("PORT", 8080), jwtSecret: env.GetEnvString("JWT_SECRET", "some-secret-1213123"), models: models, } if err := serve(app); err != nil { log.Fatal(err) } }Here we load environment variables, initialize the database connection, create anapplicationstruct and start the server using theservefunction.The application struct will be used to pass the dependencies around without having global variables.We then start the server using theservefunction.
Now we can start the server by running the following command:
airYou should see the following output:
Starting server on port 8080This means that the server is running and listening for incoming requests on port 8080.
CRUD for Events
Currently we have no routes so let's add some.
- Set Up Event Routes:Add routes to handle HTTP requests for event operations in your
routes.gofile.func (app *application) routes() http.Handler { g := gin.Default() v1 := g.Group("/api/v1") { v1.POST("/events", app.createEvent) v1.GET("/events", app.getAllEvents) v1.GET("/events/:id", app.getEvent) v1.PUT("/events/:id", app.updateEvent) v1.DELETE("/events/:id", app.deleteEvent) } return g }We define a route group/api/v1to version our API. Within this group, we map HTTP methods and paths to the corresponding handler functions for event operations. This structure helps organize routes and makes it easier to manage API versions. - Implement Event Handlers:Create a
events.gofile in thecmd/apifolder and add the following methods.Create Eventpackage main import ( "net/http" "rest-api-in-gin/internal/database" "strconv" "github.com/gin-gonic/gin" ) func (app *application) createEvent(c *gin.Context) { var event database.Event if err := c.ShouldBindJSON(&event); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err := app.models.Events.Insert(&event) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create event"}) return } c.JSON(http.StatusCreated, event) }This handler manages the creation of a new event. It binds the incoming JSON request body to anEventstruct, validates the data, and calls theInsertmethod on theEventModelto add the event to the database. If successful, it returns a201 Createdstatus with the created event data.Get All Eventsfunc (app *application) getAllEvents(c *gin.Context) { events, err := app.models.Events.GetAll() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve events"}) return } c.JSON(http.StatusOK, events) }This handler retrieves all events. It calls theGetAllmethod on theEventModelto fetch all events from the database. If successful, it returns a200 OKstatus with the list of events.Get Eventfunc (app *application) getEvent(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event ID"}) return } event, err := app.models.Events.Get(id) if event == nil { c.JSON(http.StatusNotFound, gin.H{"error": "Event not found"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve event"}) return } c.JSON(http.StatusOK, event) }This handler retrieves a specific event by its ID. It extracts the event ID from the URL parameters, validates it, and calls theGetmethod on theEventModelto fetch the event from the database. If the event is found, it returns a200 OKstatus with the event data else it returns a404 Not Foundstatus.Update Eventfunc (app *application) updateEvent(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event ID"}) return } existingEvent, err := app.models.Events.Get(id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve event"}) return } if existingEvent == nil { c.JSON(http.StatusNotFound, gin.H{"error": "Event not found"}) return } updateEvent := &database.Event{} if err := c.ShouldBindJSON(&updateEvent); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updateEvent.Id = id if err := app.models.Events.Update(updateEvent); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update event"}) return } c.JSON(http.StatusOK, updateEvent) }This handler updates an existing event. It extracts and validates the event ID from the URL parameters, checks if the event exists, binds the incoming JSON request body to anEventstruct, and calls theUpdatemethod on theEventModelto update the event in the database. If successful, it returns a200 OKstatus with the updated event data.Delete Eventfunc (app *application) deleteEvent(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event ID"}) return } if err := app.models.Events.Delete(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete event"}) return } c.JSON(http.StatusNoContent, nil) }This handler deletes a specific event by its ID. It extracts and validates the event ID from the URL parameters and calls theDeletemethod on theEventModelto remove the event from the database. If successful, it returns a204 No Contentstatus. - Implement Event Model Methods:Define the methods for the
EventModelto interact with the database. Open up theevents.gofile in thedatabasefolder. Update the imports and below theEventstruct add the following methods.Update importspackage database import ( "database/sql" "context" "time" )Insert Methodfunc (m EventModel) Insert(event *Event) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := "INSERT INTO events (owner_id, name, description, date, location) VALUES ($1, $2, $3, $4, $5) RETURNING id" err := m.DB.QueryRowContext(ctx, query, event.OwnerId, event.Name, event.Description, event.Date, event.Location).Scan(&event.Id) if err != nil { return err } return nil }This function inserts a new event into theeventstable. It usesQueryRowContext, which executes the query with a context that includes a 3-second timeout, ensuring the operation doesn't hang indefinitely. If there is no error we add the id to the event and returnnil.GetAll Methodfunc (m EventModel) GetAll() ([]*Event, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := "SELECT * FROM events" rows, err := m.DB.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() events := []*Event{} for rows.Next() { var event Event err := rows.Scan(&event.Id, &event.OwnerId, &event.Name, &event.Description, &event.Date, &event.Location) if err != nil { return nil, err } events = append(events, &event) } if err = rows.Err(); err != nil { return nil, err } return events, nil }We retrieve all records from theeventstable. We then iterate over the result set and append each event to theeventsslice. If the query fails, we return an error.Get Methodfunc (m EventModel) Get(id int) (*Event, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := "SELECT * FROM events WHERE id = $1" row := m.DB.QueryRowContext(ctx, query, id) var event Event err := row.Scan(&event.Id, &event.OwnerId, &event.Name, &event.Description, &event.Date, &event.Location) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, err } return &event, nil }This function retrieves a specific record from theeventstable where theidmatches the provided value. It maps the result to theEventstruct fields. We check if the event is not found and return nil if it is not found.Update Methodfunc (m EventModel) Update(event *Event) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := "UPDATE events SET name = $1, description = $2, date = $3, location = $4 WHERE id = $5" _, err := m.DB.ExecContext(ctx, query, event.Name, event.Description, event.Date, event.Location, event.Id) if err != nil { return err } return nil }This function updates an existing record in theeventstable. It uses theSETclause to specify the columns to be updated and their new values. It ensures only the record with the specifiedidis updated. If the update fails, it returns an error.Delete Methodfunc (m EventModel) Delete(id int) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := "DELETE FROM events WHERE id = $1" _, err := m.DB.ExecContext(ctx, query, id) if err != nil { return err } return nil }Removes a record from theeventstable where theidmatches the provided value. Returns an error if the deletion fails.You now have a complete CRUD functionality for events. Let's test it.
Creating a User
To be able to connect events with attendees and have events have an owner we need to create a user.
Start adding a new route to the routes.go file.
func (app *application) routes() http.Handler {
... rest of the routes
v1.POST("/auth/register", app.registerUser)
}We will group the routes under /auth and use the POST register method to register a new user, later we will add a login route.
Create a new handlers file called auth.go in the cmd/api folder and add the following code:
package main
import (
"net/http"
"rest-api-in-gin/internal/database"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
type registerRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Name string `json:"name" binding:"required,min=2"`
}
func (app *application) registerUser(c *gin.Context) {
var register registerRequest
if err := c.ShouldBindJSON(®ister); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Hash the password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(register.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Something went wrong"})
return
}
register.Password = string(hashedPassword)
user := database.User{
Email: register.Email,
Password: register.Password,
Name: register.Name,
}
err = app.models.Users.Insert(&user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, user)
}
Explanation of the `registerUserHandler` Method
The registerUserHandler function is responsible for handling user registration requests. Here's a breakdown of its functionality:
- Data Binding and Validation: The function begins by binding the incoming JSON request body to a
registerRequeststruct. This ensures that the data is properly formatted and meets the required criteria, such as a valid email format and minimum password length. - Password Hashing: To enhance security, the user's password is hashed using the
bcryptlibrary. This step is crucial as it ensures that the password is not stored in plain text in the database. - User Creation: A new
Userinstance is created with the provided email, hashed password, and name. This instance is then inserted into the database. - Response Handling: If the user is successfully created, the function responds with a
201 Createdstatus and the user data. If any errors occur during the process, appropriate error messages are returned to the client.
Open user.go in the database package and update the imports and add the insert method to the UserModel struct.
import (
"context"
"database/sql"
"time"
)func (m *UserModel) Insert(user *User) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
stmt := `INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING id`
err := m.DB.QueryRowContext(ctx, stmt, user.Email, user.Password, user.Name).Scan(&user.Id)
if err != nil {
return err
}
return nil
}Here we insert the user into the database and return an error if there is one.
Testing Events and Users with Curl
We will use Curl a command line tool to test http requests.
Here are all curl commands that you can paste into your terminal. To test the events.
Let's start by creating a new user.
curl -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "password",
"name": "Test User"
}'After you have created a user you can create an event.
curl -X POST http://localhost:8080/api/v1/events \
-H "Content-Type: application/json" \
-d '{
"name": "Go Conference",
"ownerId": 1,
"description": "A conference about Go programming",
"date": "2025-05-20",
"location": "San Francisco"
}' \
-w "\nHTTP Status: %{http_code}\n"If everything went well you should see the following output:
{
"id": 1,
"ownerId": 1,
"name": "Go Conference",
"description": "A conference about Go programming",
"date": "2025-05-20",
"location": "San Francisco"
}
HTTP Status: 201 CreatedWe can also retrieve all events.
curl -X GET http://localhost:8080/api/v1/events \
-H "Content-Type: application/json" \
-w "\nHTTP Status: %{http_code}\n"To retrieve a specific event we can use the following command:
curl -X GET http://localhost:8080/api/v1/events/1 \
-H "Content-Type: application/json" \
-w "\nHTTP Status: %{http_code}\n"Lets update the event.
curl -X PUT http://localhost:8080/api/v1/events/1 \
-H "Content-Type: application/json" \
-d '{
"name": "Go Conference",
"ownerId": 1,
"description": "A conference about Go programming",
"date": "2025-05-20",
"location": "New York"
}' \
-w "\nHTTP Status: %{http_code}\n"If you try to retrieve the event again you should see that the location has been updated.
The last thing we can do is delete the event.
curl -X DELETE http://localhost:8080/api/v1/events/1 \
-H "Content-Type: application/json" \
-w "\nHTTP Status: %{http_code}\n"If you try to retrieve the event again you should get back a 404 not found error.
Connecting Events with Attendees
We want users to be able to attend events.
- Set Up Attendee Routes:Add routes to handle HTTP requests for attendee operations in your
routes.gofile.func (app *application) routes() http.Handler { g := gin.Default() v1 := g.Group("/api/v1") { // ... rest of the routes ... v1.POST("/events/:id/attendees/:userId", app.addAttendeeToEvent) v1.GET("/events/:id/attendees", app.getAttendeesForEvent) } return g } - Implement Attendee Handlers:Open up
events.goin thecmd/apifolder and add the following code:func (app *application) addAttendeeToEvent(c *gin.Context) { eventId, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event ID"}) return } userId, err := strconv.Atoi(c.Param("userId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) return } event, err := app.models.Events.Get(eventId) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve event"}) return } if event == nil { c.JSON(http.StatusNotFound, gin.H{"error": "Event not found"}) return } userToAdd, err := app.models.Users.Get(userId) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve user"}) return } if userToAdd == nil { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } existingAttendee, err := app.models.Attendees.GetByEventAndAttendee(event.Id, userToAdd.Id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve attendee"}) return } if existingAttendee != nil { c.JSON(http.StatusConflict, gin.H{"error": "Attendee already exists"}) return } attendee := database.Attendee{ EventId: event.Id, UserId: userToAdd.Id, } _, err = app.models.Attendees.Insert(&attendee) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add attendee"}) return } c.JSON(http.StatusCreated, attendee) }c.Param("id")andc.Param("userId")are used to extract URL parameters for the event and user IDs, respectively.- The function checks if the event and user exist in the database. If not, it returns a
404 Not Foundresponse. - It verifies if the attendee already exists for the event. If so, it returns a
409 Conflictresponse. - The
Insertmethod is called to add the attendee to the database if all checks pass. - If any operation fails, appropriate HTTP error responses are returned.
Add a new handler function to get the attendees for an event.func (app *application) getAttendeesForEvent(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event ID"}) return } users, err := app.models.Attendees.GetAttendeesByEvent(id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, users) }- The method extracts the event ID from the URL parameters using
c.Param("id")and converts it to an integer. If the conversion fails, it returns a400 Bad Requestresponse indicating an invalid event ID. - It calls the
GetAttendeesByEventmethod from theAttendeesmodel to fetch a list of users attending the specified event. - If an error occurs during data retrieval, a
500 Internal Server Errorresponse is returned with the error message. - If the data retrieval is successful, a
200 OKresponse is returned along with the list of attendees.
- Implement Attendee Model Methods:Define the methods for the
AttendeeModelinattendees.go.Insert MethodOpenattendees.goin thedatabasefolder and add the following code:Update the imports.import ( "context" "database/sql" "time" )Add the insert method to theAttendeeModelstruct.Insert Methodpackage database func (m *AttendeeModel) Insert(attendee *Attendee) (*Attendee, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := `INSERT INTO attendees (event_id, user_id) VALUES ($1, $2) RETURNING id` err := m.DB.QueryRowContext(ctx, query, attendee.EventId, attendee.UserId).Scan(&attendee.Id) if err != nil { return nil, err } return attendee, nil }Here we insert the attendee into the database with the provided user ID, event ID and return an error if there is one.GetByEventAndAttendee Methodfunc (m *AttendeeModel) GetByEventAndAttendee(eventId, userId int) (*Attendee, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := `SELECT * FROM attendees WHERE event_id = $1 AND user_id = $2` var attendee Attendee err := m.DB.QueryRowContext(ctx, query, eventId, userId).Scan(&attendee.Id, &attendee.UserId, &attendee.EventId) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, err } return &attendee, nil }This method retrieves an attendee record from the database based on the provided event ID and user ID.GetAttendeesByEvent Methodfunc (m AttendeeModel) GetAttendeesByEvent(eventId int) ([]User, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := ` SELECT u.id, u.name, u.email FROM users u JOIN attendees a ON u.id = a.user_id WHERE a.event_id = $1 ` rows, err := m.DB.QueryContext(ctx, query, eventId) if err != nil { return nil, err } defer rows.Close() var users []User for rows.Next() { var user User err := rows.Scan(&user.Id, &user.Name, &user.Email) if err != nil { return nil, err } users = append(users, user) } return users, nil }This method retrieves a list of users attending a specific event by joining theusersandattendeestables. - Add get user by id methodAdd the following method to the
UserModelstruct inusers.go.func (m *UserModel) Get(id int) (*User, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := `SELECT * FROM users WHERE id = $1` var user User err := m.DB.QueryRowContext(ctx, query, id).Scan(&user.Id, &user.Email, &user.Name, &user.Password) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, err } return &user, nil }This method retrieves a user by their ID from the database.
Delete Attendee from Event & Get Events for User
We can now add attendess to an event and retrieve the attendees for an event. However it would be nice if we could delete an attendee from an event and retrieve the events for a user.
- Routes AdditionAdd the following routes to handle new functionalities in your
routes.gofile.v1.DELETE("/events/:id/attendees/:userId", app.deleteAttendeeFromEvent) v1.GET("/attendees/:id/events", app.getEventsByAttendee) - Handler FunctionsImplement the following handler functions in the
events.goin thecmd/apifolder.Remove Attendee from Event:func (app *application) deleteAttendeeFromEvent(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event ID"}) return } userId, err := strconv.Atoi(c.Param("userId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) return } err = app.models.Attendees.Delete(userId, id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete attendee"}) return } c.JSON(http.StatusNoContent, nil) }- Extracts
id(attendee ID) andeventIdfrom the URL parameters. - Validates the IDs and returns a
400 Bad Requestif they are invalid. - Calls the
Deletemethod on theAttendeeModelto remove the attendee. - Returns a
204 No Contentstatus if the operation is successful, indicating that the request was successful but there is no content to send back.
Get Events for Attendee:func (app *application) getEventsByAttendee(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid attendee ID"}) return } events, err := app.models.Events.GetByAttendee(id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, events) }- This function retrieves all events an attendee is attending.
- It extracts
id(attendee ID) from the URL parameters. - Validates the ID and returns a
400 Bad Requestif it is invalid. - Calls the
GetByAttendeemethod on theEventModelto fetch the events. - Returns a
200 OKstatus with the list of events if the operation is successful.
- Database MethodsDelete Method:Open
attendees.goin thedatabasefolder and add this method:func (m *AttendeeModel) Delete(userId, eventId int) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := `DELETE FROM attendees WHERE user_id = $1 AND event_id = $2` _, err := m.DB.ExecContext(ctx, query, userId, eventId) if err != nil { return err } return nil }This method deletes an attendee from an event with the provided user ID and event ID.Get Events for Attendee:func (m EventModel) GetByAttendee(attendeeId int) ([]Event, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := ` SELECT e.id, e.owner_id, e.name, e.description, e.date, e.location FROM events e JOIN attendees a ON e.id = a.event_id WHERE a.user_id = $1 ` rows, err := m.DB.QueryContext(ctx, query, attendeeId) if err != nil { return nil, err } defer rows.Close() var events []Event for rows.Next() { var event Event err := rows.Scan(&event.Id, &event.OwnerId, &event.Name, &event.Description, &event.Date, &event.Location) if err != nil { return nil, err } events = append(events, event) } return events, nil }This method retrieves all events a user is attending with the provided attendee ID, joining theeventsandattendeestables to get the relevant data.
Testing the API
We created a user before that we can use to test the API.
Lets create a new event.
curl -X POST http://localhost:8080/api/v1/events -H "Content-Type: application/json" -d '{"name": "Test Event", "ownerId": 1, "description": "This is a test event", "date": "2025-10-01", "location": "Test Location"}' -w "\nHTTP Status: %{http_code}\n"We can now add the user to the event. Take the id from the user and the event id. (Your ids may be different check the response from the previous requests)
curl -X POST http://localhost:8080/api/v1/events/1/attendees/1 -H "Content-Type: application/json" -w "\nHTTP Status: %{http_code}\n"We should now get back an attendee this means the user has been added to the event. If we try the same request again we will get {"error":"Attendee already exists"}.
This will add the user to the event.
Lets get all the attendees for the event.
curl http://localhost:8080/api/v1/events/1/attendeesAdding Authentication
Currently, anyone can create, update, and delete events. It would be nice if we could restrict these operations to only be performed by authenticated users.
- Routes AdditionAdd a new route in
routes.goto handle the authentication.v1.POST("/auth/login", app.login) - Handler FunctionsAdd the following code to
auth.goin thecmd/apifolder to handle login.Update Importsimport ( "fmt" "net/http" "rest-api-in-gin/internal/database" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" "golang.org/x/crypto/bcrypt" )Create Login Request, Response, and Handlertype loginRequest struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=8"` } type loginResponse struct { Token string `json:"token"` } func (app *application) login(c *gin.Context) { var auth loginRequest if err := c.ShouldBindJSON(&auth); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } existingUser, err := app.models.Users.GetByEmail(auth.Email) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } err = bcrypt.CompareHashAndPassword([]byte(existingUser.Password), []byte(auth.Password)) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid password"}) return } token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "userId": existingUser.Id, "exp": time.Now().Add(time.Hour * 72).Unix(), // Token expires in 72 hours }) tokenString, err := token.SignedString([]byte(app.jwtSecret)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Error generating token"}) return } c.JSON(http.StatusOK, loginResponse{Token: tokenString}) }Explanation of the Login Function- The function begins by binding the incoming JSON request body to a
loginRequeststruct. This ensures that the data is properly formatted and meets the required criteria, such as a valid email format and a minimum password length of 8 characters. - It checks if the user exists in the database by calling the
GetByEmailmethod on theUserModel. If the user is not found, a404 Not Foundresponse is returned. - The function uses the
bcryptlibrary to compare the provided password with the stored hashed password. If the passwords do not match, a401 Unauthorizedresponse is returned. - Upon successful authentication, a JWT token is generated using the
jwtlibrary. The token includes the user's ID and an expiration time (e.g., 72 hours from the time of issuance). - The generated token is returned to the client in a
200 OKresponse. This token can then be used by the client to access protected routes.
- Database MethodsAdd
getByEmailto theUserModel. Openusers.goin thedatabaseand replace theGetmethod with the following code.func (m *UserModel) getUser(query string, args ...interface{}) (*User, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() var user User err := m.DB.QueryRowContext(ctx, query, args...).Scan(&user.Id, &user.Email, &user.Name, &user.Password) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, err } return &user, nil } func (m *UserModel) Get(id int) (*User, error) { query := `SELECT * FROM users WHERE id = $1` return m.getUser(query, id) } func (m *UserModel) GetByEmail(email string) (*User, error) { query := `SELECT * FROM users WHERE email = $1` return m.getUser(query, email) }Here we did some refactoring and created a new method calledgetUser, notice the...interface{}in the method signature. This allows us to pass in multiple arguments to the method. Then we have theGetandGetByEmailmethods that we can use to get a user by id or email. This refactoring reduces code duplication and centralizes the logic for querying and handling user data. - MiddlewareWe can now add middleware to our API to restrict access to certain routes.Add a new file called
middleware.goincmd/apiand add the following code:package main import ( "net/http" "strings" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" ) func (app *application) AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) c.Abort() return } tokenString := strings.TrimPrefix(authHeader, "Bearer ") if tokenString == authHeader { c.JSON(http.StatusUnauthorized, gin.H{"error": "Bearer token is required"}) c.Abort() return } token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } return []byte(app.jwtSecret), nil }) if err != nil || !token.Valid { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) c.Abort() return } claims, ok := token.Claims.(jwt.MapClaims) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) c.Abort() return } userId := claims["userId"].(float64) user, err := app.models.Users.Get(int(userId)) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized access"}) c.Abort() return } c.Set("user", user) c.Next() } }Explanation of the Middleware- Retrieve the Authorization Header: The middleware starts by reading the
Authorizationheader from the incoming request. This header should contain the JWT. If the header is missing, the middleware responds with a401 Unauthorizedstatus and stops further processing by callingc.Abort(). - Extract the Bearer Token: The JWT is expected to be in the format
Bearer {token}. The middleware removes theBearerprefix to extract the actual token. If the token is not in the expected format, it responds with a401 Unauthorizedstatus and aborts the request. - Parse and Validate the JWT: The middleware uses the
jwt.Parsefunction to decode and validate the token. It checks that the token's signing method is HMAC, which is a common method for signing JWTs. ThejwtSecretis used to verify the token's signature, ensuring it hasn't been tampered with. - Handle Invalid Tokens: If the token is invalid or an error occurs during parsing, the middleware responds with a
401 Unauthorizedstatus and aborts the request. - Extract User Information: If the token is valid, the middleware extracts the user ID from the token's claims and retrieves the corresponding user from the database. The user is then set in the request context using
c.Set("user", user). This allows other handlers in the chain to access the authenticated user. - Allow the Request to Proceed: If the token is valid, the middleware calls
c.Next(), allowing the request to proceed to the next handler in the chain.
- Protect RoutesWe can now add the middleware to our routes. Start by creating a new group of routes that we want to protect, then add the middleware to the group.Your code should now look like this:
package main import ( "net/http" "github.com/gin-gonic/gin" ) func (app *application) routes() http.Handler { g := gin.Default() v1 := g.Group("/api/v1") { v1.GET("/events", app.getAllEvents) v1.GET("/events/:id", app.getEvent) v1.GET("/events/:id/attendees", app.getAttendeesForEvent) v1.GET("/attendees/:id/events", app.getEventsByAttendee) v1.POST("/register", app.registerUser) v1.POST("/login", app.login) } authGroup := v1.Group("/") authGroup.Use(app.AuthMiddleware()) { authGroup.POST("/events", app.createEvent) authGroup.PUT("/events/:id", app.updateEvent) authGroup.DELETE("/events/:id", app.deleteEvent) authGroup.POST("/events/:id/attendees/:userId", app.addAttendeeToEvent) authGroup.DELETE("/events/:id/attendees/:userId", app.deleteAttendeeFromEvent) } return g } - Testing the APIWe can now test the API by trying to create a new event without a valid token.
curl -X POST http://localhost:8080/api/v1/events -H "Content-Type: application/json" -d '{"name": "Test Event", "ownerId": 1, "description": "This is a test event", "date": "2025-01-01", "location": "Test Location"}' -w "\nHTTP Status: %{http_code}\n"This should return a401 Unauthorizedstatus. With the message{"error":"Authorization header is required"}Login and get a tokenWe can now login and get a token.curl -X POST http://localhost:8080/api/v1/login -H "Content-Type: application/json" -d '{"email": "[email protected]", "password": "password"}' -w "\nHTTP Status: %{http_code}\n"This will return a token that we can use to authenticate our requests.Use the TokenWe can now use the token to create a new event. To add the token to the request we need to add it to theAuthorizationheader. The format should be-H "Authorization: Bearer {token}".curl -X POST http://localhost:8080/api/v1/events -H "Content-Type: application/json" -H "Authorization: Bearer {token}" -d '{"name": "Test Event", "ownerId": 1, "description": "This is a test event", "date": "2025-01-01", "location": "Test Location"}' -w "\nHTTP Status: %{http_code}\n"should now succeed and return a201 Createdstatus.
Adding Authorization
Currently a user can delete and update any event. We want to restrict this to only allow the user to do it if they are the owner of the event.
First we need to add a helper function to get the user from the context. Create a new file called context.go in cmd/api and add the following code:
package main
import (
"rest-api-in-gin/internal/database"
"github.com/gin-gonic/gin"
)
func (app *application) GetUserFromContext(c *gin.Context) *database.User {
contextUser, exists := c.Get("user")
if !exists {
return &database.User{}
}
user, ok := contextUser.(*database.User)
if !ok {
return &database.User{}
}
return user
}Here we are getting the user from the context and returning it. If the user is not found we return an empty user.
Let's start with the handler for updating an event, open up events.go in cmd/api and replace the updateEvent method with the following code.
func (app *application) updateEvent(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event ID"})
return
}
user := app.GetUserFromContext(c)
existingEvent, err := app.models.Events.Get(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve event"})
return
}
if existingEvent == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Event not found"})
return
}
if existingEvent.OwnerId != user.Id {
c.JSON(http.StatusForbidden, gin.H{"error": "You are not authorized to update this event"})
return
}
updateEvent := &database.Event{
Id: id,
}
if err := c.ShouldBindJSON(&updateEvent); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := app.models.Events.Update(updateEvent); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update event"})
return
}
c.JSON(http.StatusOK, updateEvent)
}We get the user from the context and check if the user is the owner of the event. If they are not we return a 403 Forbidden status.
Lets do the same for deleting an event, adding an attendee and deleting an attendee from an event.
Delete Event
func (app *application) deleteEvent(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event ID"})
return
}
user := app.GetUserFromContext(c)
existingEvent, err := app.models.Events.Get(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve event"})
return
}
if existingEvent == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Event not found"})
return
}
if user.Id != existingEvent.OwnerId {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized access"})
return
}
if err := app.models.Events.Delete(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete event"})
return
}
c.JSON(http.StatusNoContent, nil)
}Add Attendee to Event
func (app *application) addAttendeeToEvent(c *gin.Context) {
eventId, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event ID"})
return
}
userId, err := strconv.Atoi(c.Param("userId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
event, err := app.models.Events.Get(eventId)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve event"})
return
}
if event == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Event not found"})
return
}
userToAdd, err := app.models.Users.Get(userId)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve user"})
return
}
user := app.GetUserFromContext(c)
if user.Id != event.OwnerId {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized access"})
return
}
if userToAdd == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
existingAttendee, err := app.models.Attendees.GetByEventAndAttendee(event.Id, userToAdd.Id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve attendee"})
return
}
if existingAttendee != nil {
c.JSON(http.StatusConflict, gin.H{"error": "Attendee already exists"})
return
}
attendee := database.Attendee{
EventId: event.Id,
UserId: userToAdd.Id,
}
_, err = app.models.Attendees.Insert(&attendee)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add attendee"})
return
}
c.JSON(http.StatusCreated, attendee)
}Delete Attendee from Event
func (app *application) deleteAttendeeFromEvent(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event ID"})
return
}
userId, err := strconv.Atoi(c.Param("userId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
event, err := app.models.Events.Get(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve event"})
return
}
user := app.GetUserFromContext(c)
if user.Id != event.OwnerId {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized access"})
return
}
err = app.models.Attendees.Delete(userId, id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete attendee"})
return
}
c.JSON(http.StatusNoContent, nil)
}We can also use the same method when creating an event. To set the owner of the event. Remmeber before we were setting the owner to the user id from the request.
Create Event
func (app *application) createEvent(c *gin.Context) {
var event database.Event
if err := c.ShouldBindJSON(&event); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user := app.GetUserFromContext(c)
event.OwnerId = user.Id
err := app.models.Events.Insert(&event)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create event"})
return
}
c.JSON(http.StatusCreated, event)
}We get the user from the context and set the owner of the event to the user id.
We also need to update our event model so binding for the owner id is not required. Open up events.go in internal/database and remove the OwnerId field from the Event struct.
The event model should now look like this:
type Event struct {
Id int `json:"id"`
Name string `json:"name" binding:"required,min=3"`
Description string `json:"description" binding:"required,min=10"`
Date string `json:"date" binding:"required,datetime=2006-01-02"`
Location string `json:"location" binding:"required,min=3"`
OwnerId int `json:"ownerId"`
}Swagger
Swagger is an API documentation tool that allows you to generate interactive API documentation from your code.
To add Swagger to our API we can use the gin-swagger package.
Add the following to your main.go file in cmd/api import the docs and add the swagger comments.
import (
"database/sql"
"log"
_ "rest-api-in-gin/docs"
"rest-api-in-gin/internal/database"
"rest-api-in-gin/internal/env"
_ "github.com/joho/godotenv/autoload"
_ "github.com/mattn/go-sqlite3"
)
// @title Go Gin Rest API
// @version 1.0
// @description A rest API in Go using Gin framework.
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Enter your bearer token in the format **Bearer <token>**
// Apply the security definition to your endpoints
// @security BearerAuth
type application struct {
port int
jwtSecret string
models database.Models
}
// -- rest of the code --
Open up routes.go and add the following route to serve the swagger.json file.
import (
"net/http"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
func (app *application) routes() http.Handler {
g := gin.Default()
v1 := g.Group("/api/v1")
...rest of the routes...
g.GET("/swagger/*any", func(c *gin.Context) {
if c.Request.RequestURI == "/swagger/" {
c.Redirect(302, "/swagger/index.html")
return
}
ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.URL("http://localhost:8080/swagger/doc.json"))(c)
})
return g
}
This code snippet sets up a route to serve the Swagger documentation. It also redirects the root /swagger/ path to the Swagger UI. So it will be available at http://localhost:8080/swagger/.
Now we just need to document our handlers.
events.go will now look like this:
package main
import (
"net/http"
"rest-api-in-gin/internal/database"
"strconv"
"github.com/gin-gonic/gin"
_ "github.com/joho/godotenv/autoload"
_ "github.com/mattn/go-sqlite3"
)
// GetEvents returns all events
//
// @Summary Returns all events
// @Description Returns all events
// @Tags events
// @Accept json
// @Produce json
// @Success 200 {object} []database.Event
// @Router /api/v1/events [get]
func (app *application) getAllEvents(c *gin.Context) {
events, err := app.models.Events.GetAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retreive events"})
}
c.JSON(http.StatusOK, events)
}
// GetEvent returns a single event
//
// @Summary Returns a single event
// @Description Returns a single event
// @Tags events
// @Accept json
// @Produce json
// @Param id path int true "Event ID"
// @Success 200 {object} database.Event
// @Router /api/v1/events/{id} [get]
func (app *application) getEvent(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event ID"})
}
event, err := app.models.Events.Get(id)
if event == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Event not found"})
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retreive event"})
}
c.JSON(http.StatusOK, event)
}
// CreateEvent creates a new event
//
// @Summary Creates a new event
// @Description Creates a new event
// @Tags events
// @Accept json
// @Produce json
// @Param event body database.Event true "Event"
// @Success 201 {object} database.Event
// @Router /api/v1/events [post]
// @Security BearerAuth
func (app *application) createEvent(c *gin.Context) {
var event database.Event
if err := c.ShouldBindJSON(&event); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user := app.GetUserFromContext(c)
event.OwnerId = user.Id
err := app.models.Events.Insert(&event)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create event"})
return
}
c.JSON(http.StatusCreated, event)
}
// UpdateEvent updates an existing event
//
// @Summary Updates an existing event
// @Description Updates an existing event
// @Tags events
// @Accept json
// @Produce json
// @Param id path int true "Event ID"
// @Param event body database.Event true "Event"
// @Success 200 {object} database.Event
// @Router /api/v1/events/{id} [put]
// @Security BearerAuth
func (app *application) updateEvent(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event ID"})
return
}
user := app.GetUserFromContext(c)
existingEvent, err := app.models.Events.Get(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retreive event"})
return
}
if existingEvent == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Event not found"})
return
}
if existingEvent.OwnerId != user.Id {
c.JSON(http.StatusForbidden, gin.H{"error": "You are not authorized to update this event"})
return
}
updatedEvent := &database.Event{}
if err := c.ShouldBindJSON(updatedEvent); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updatedEvent.Id = id
if err := app.models.Events.Update(updatedEvent); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update event"})
return
}
c.JSON(http.StatusOK, updatedEvent)
}
// DeleteEvent deletes an existing event
//
// @Summary Deletes an existing event
// @Description Deletes an existing event
// @Tags events
// @Accept json
// @Produce json
// @Param id path int true "Event ID"
// @Success 204
// @Router /api/v1/events/{id} [delete]
// @Security BearerAuth
func (app *application) deleteEvent(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"Error": "Invalid event ID"})
}
user := app.GetUserFromContext(c)
existingEvent, err := app.models.Events.Get(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"Error": "Failed to retreive event"})
return
}
if existingEvent == nil {
c.JSON(http.StatusNotFound, gin.H{"Error": "Event not found"})
return
}
if existingEvent.OwnerId != user.Id {
c.JSON(http.StatusForbidden, gin.H{"error": "You are not authorized to delete this event"})
return
}
if err := app.models.Events.Delete(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete event"})
}
c.JSON(http.StatusNoContent, nil)
}
// GetAttendeesForEvent returns all attendees for a given event
//
// @Summary Returns all attendees for a given event
// @Description Returns all attendees for a given event
// @Tags attendees
// @Accept json
// @Produce json
// @Param id path int true "Event ID"
// @Success 200 {object} []database.User
// @Router /api/v1/events/{id}/attendees [get]
func (app *application) getAttendeesForEvent(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event id"})
return
}
users, err := app.models.Attendees.GetAttendeesByEvent(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to to retreive attendees for events"})
return
}
c.JSON(http.StatusOK, users)
}
// AddAttendeeToEvent adds an attendee to an event
// @Summary Adds an attendee to an event
// @Description Adds an attendee to an event
// @Tags attendees
// @Accept json
// @Produce json
// @Param id path int true "Event ID"
// @Param userId path int true "User ID"
// @Success 201 {object} database.Attendee
// @Router /api/v1/events/{id}/attendees/{userId} [post]
// @Security BearerAuth
func (app *application) addAttendeeToEvent(c *gin.Context) {
eventId, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event Id"})
return
}
userId, err := strconv.Atoi(c.Param("userId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user Id"})
return
}
event, err := app.models.Events.Get(eventId)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retreive event"})
return
}
if event == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Event not found"})
}
userToAdd, err := app.models.Users.Get(userId)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retreive user"})
return
}
if userToAdd == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
}
user := app.GetUserFromContext(c)
if event.OwnerId != user.Id {
c.JSON(http.StatusForbidden, gin.H{"error": "You are not authorized to add an attendee"})
return
}
existingAttendee, err := app.models.Attendees.GetByEventAndAttendee(event.Id, userToAdd.Id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retreive attendee"})
return
}
if existingAttendee != nil {
c.JSON(http.StatusConflict, gin.H{"error": "Attendee already exists"})
return
}
attendee := database.Attendee{
EventId: event.Id,
UserId: userToAdd.Id,
}
_, err = app.models.Attendees.Insert(&attendee)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add attendee"})
return
}
c.JSON(http.StatusCreated, attendee)
}
// GetEventsByAttendee returns all events for a given attendee
//
// @Summary Returns all events for a given attendee
// @Description Returns all events for a given attendee
// @Tags attendees
// @Accept json
// @Produce json
// @Param id path int true "Attendee ID"
// @Success 200 {object} []database.Event
// @Router /api/v1/attendees/{id}/events [get]
func (app *application) getEventsByAttendee(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid attendee id"})
return
}
events, err := app.models.Attendees.GetEventsByAttendee(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get events"})
return
}
c.JSON(http.StatusOK, events)
}
// DeleteAttendeeFromEvent deletes an attendee from an event
// @Summary Deletes an attendee from an event
// @Description Deletes an attendee from an event
// @Tags attendees
// @Accept json
// @Produce json
// @Param id path int true "Event ID"
// @Param userId path int true "User ID"
// @Success 204
// @Router /api/v1/events/{id}/attendees/{userId} [delete]
// @Security BearerAuth
func (app *application) deleteAttendeeFromEvent(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event id"})
return
}
userId, err := strconv.Atoi(c.Param("userId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user id"})
return
}
event, err := app.models.Events.Get(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Something went wrong"})
return
}
if event == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Event not found"})
return
}
user := app.GetUserFromContext(c)
if event.OwnerId != user.Id {
c.JSON(http.StatusForbidden, gin.H{"error": "You are not authorized to delete an attendeeFromEvent"})
return
}
err = app.models.Attendees.Delete(userId, id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete attendee"})
return
}
c.JSON(http.StatusNoContent, nil)
}The auth.go file will now look like this:
package main
import (
"net/http"
"rest-api-in-gin/internal/database"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"golang.org/x/crypto/bcrypt"
)
type loginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
}
type loginResponse struct {
Token string `json:"token"`
}
type registerRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Name string `json:"name" binding:"required,min=2"`
}
// RegisterUser registers a new user
// @Summary Registers a new user
// @Description Registers a new user
// @Tags auth
// @Accept json
// @Produce json
// @Param user body registerRequest true "User"
// @Success 201 {object} database.User
// @Router /api/v1/auth/register [post]
func (app *application) registerUser(c *gin.Context) {
var register registerRequest
if err := c.ShouldBindJSON(®ister); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(register.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"Error": "Something went wrong"})
return
}
register.Password = string(hashedPassword)
user := database.User{
Email: register.Email,
Password: register.Password,
Name: register.Name,
}
err = app.models.Users.Insert(&user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create user"})
return
}
c.JSON(http.StatusCreated, user)
}
// Login logs in a user
//
// @Summary Logs in a user
// @Description Logs in a user
// @Tags auth
// @Accept json
// @Produce json
// @Param user body loginRequest true "User"
// @Success 200 {object} loginResponse
// @Router /api/v1/auth/login [post]
func (app *application) login(c *gin.Context) {
var auth loginRequest
if err := c.ShouldBindJSON(&auth); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
existingUser, err := app.models.Users.GetByEmail(auth.Email)
if existingUser == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Something went wrong"})
return
}
err = bcrypt.CompareHashAndPassword([]byte(existingUser.Password), []byte(auth.Password))
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
return
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"userId": existingUser.Id,
"exp": time.Now().Add(time.Hour * 72).Unix(),
})
tokenString, err := token.SignedString([]byte(app.jwtSecret))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error generating token"})
return
}
c.JSON(http.StatusOK, loginResponse{Token: tokenString})
}Below are the Swagger tags used in the code and their purposes:
@Summary: A short description of the API endpoint.@Description: A detailed description of the API endpoint.@Tags: The category of the API endpoint.@Accept: The media type the API accepts.@Produce: The media type the API produces.@Param: The parameters of the API endpoint.@Success: The expected response of the API endpoint.@Router: The HTTP method and path of the API endpoint.@Security: The security scheme used for the API endpoint.
Run swag init --dir cmd/api --parseDependency --parseInternal --parseDepth 1 to generate the Swagger documentation.
If you open up localhost:8080/swagger/index.html in your browser, you will see the Swagger UI with all the API endpoints.

Trying our api with swagger
Go to the login endpoint and click the button Try it out Fill in the email and password and click Execute.

You will get a token in the response.

Copy the token and scroll upp to The Authorization button and click it.
Write Bearer in the Value field and paste the token after it and click Authorize.

You are now logged in and can try any endpoint that requires authorization.
Conclusion
In this tutorial, we successfully built a REST API in Go using the Gin framework. Our project included event management system that allows for the creation and management of users, events, and attendees. We implemented JWT-based authentication to secure our API, ensuring that only authorized users can perform certain actions.
Additionally, we enhanced our API with Swagger documentation, providing clear and interactive descriptions of our API endpoints.
We organized our code by creating models, handlers, routes, and middleware, each responsible for specific aspects of the application, ensuring a clean and maintainable codebase.
Full source code can be found here Github