Creating a Scalable API with Go, Gin, and MongoDB II

Creating a Scalable API with Go, Gin, and MongoDB II

In the previous tutorial, you learned about setting up your project using a package-oriented design for integrations and setting up the database for user interactions.

Now that you have the foundation, you can focus on making changes and adding features to meet your requirements.

This part of the tutorial will cover:

  1. Setting Handlers for user requests

  2. Creating routes for API endpoints

  3. Implementing the endpoint for sign-up request

  4. Implementing the endpoint for sign-in request

  5. Implementing a secure endpoint

Setting Handlers for User Requests

To process your API requests, you need to compute logic using methods.

Methods in Go are not just any function - they require a receiver to be invoked and are defined explicitly on struct types. You can create these methods to help with your request processing.

To reach your objective, you can use a repository pattern in the handlers package, similar to how you organize database query methods in the query package. You can also explore this design patterns to gain a deeper understanding.

So, you can start building a structured approach to creating your handlers:


package handlers

import (
 "fmt"

 "github.com/gin-gonic/gin"
 "github.com/akinbyte/go-app/modules/config"
 "github.com/akinbyte/go-app/modules/database"
 "github.com/akinbyte/go-app/modules/database/query"
 "go.mongodb.org/mongo-driver/mongo"
)

type GoApp struct {
 App config.GoAppTools
 DB  database.DBRepo
}

func NewGoApp(app config.GoAppTools, db mongo.Client) GoApp {
 return &GoApp{
 App: app,
 DB:  query.NewGoAppDB(app, db),
 }
}

func (ga GoApp) Home() gin.HandlerFunc {
 return func(ctx gin.Context) {
        ctx.JSON(http.StatusOK, gin.H{"resp": "Welcome to Go App home page"})
 }
}

In the code above, you designed a GoApp struct with two parts: App and DB. App is a GoAppTools type, and DB is an interface of the DBRepo type, which holds query methods for computed handlers.

The function NewGoApp takes pointers to app and db as input and returns a pointer to the GoApp struct. This function helps pass values in the main package.

Your handler method uses the GoApp struct to manage user requests for the app homepage. It returns a gin.HandlerFunc is an anonymous function using a gin.Context pointer parameter to handle middleware flow, JSON requests validation, and HTTP response status code 200.

To make your application stable, scalable, and easy to maintain, you need to modify your code by adding a layer of abstraction created in the handler package being used to handle requests to the web application in main.go.


Current code

//......
client := driver.Connection(uri)
defer func() {
 if err = client.Disconnect(context.TODO()); err != nil {
 app.ErrorLogger.Fatal(err)
 return
 }
}()
appRouter := gin.New()
appRouter.GET("/", func (ctx *gin.Context) {
 app.InfoLogger.Println("Creating a scalable web application with Gin")
})

Updated code

//........
// connecting to the database
client := driver.Connection(uri)
defer func() {
 if err = client.Disconnect(context.TODO()); err != nil {
   app.ErrorLogger.Fatal(err)
   return
 }
}()
appRouter := gin.New()
goApp := handlers.NewGoApp(&app, client)
Routes(appRouter, goApp)

//........

First, you can create a Gin instance and call the NewGoApp function from the handlers package. This function requires parameters such as a pointer to an app struct and a MongoDB client. Using these pointers, you can implement methods that provide access to all the struct methods through the function's return value.

Afterward, call the define Routes function with the Gin instance and the GoApp type returned by the handlers package. We'll provide more details on this later.

As mentioned earlier, updating your code is critical to creating a scalable application. This approach will help you modularize your code, making it more organized and easier to maintain.

Creating Routes for API Endpoints

You aim to link each route's endpoint to its respective handler, safeguarding all user requests with default security measures, which are Recovery() and Logger() functions.

To make this happen, you will develop a function named Routes with two parameters. The first parameter, r, is a pointer to the Gin framework's instance, containing the muxer, default middleware, and configuration settings. The second parameter, g is a pointer to the GoApp function within the package.

You will also save user data as cookies to utilize it in other handler methods.

To accomplish this, you will install the gin session package as shown below:

go get github.com/gin-contrib/sessions
go get github.com/gin-contrib/sessions/cookie

You have finished the task, and now you can add code for the Routes function configuration:


package main

import (
 "github.com/gin-contrib/sessions"
 "github.com/gin-contrib/sessions/cookie"
 "github.com/gin-gonic/gin"
 "github.com/akinbyte/go-app/handlers"
)

func Routes(r gin.Engine, g handlers.GoApp) {
 router := r.Use(gin.Logger(), gin.Recovery())

 router.GET("/", g.Home())

 // set up for storing details as cookies
 cookieData := cookie.NewStore([]byte("go-app"))
 router.Use(sessions.Sessions("session", cookieData))
}

Use the Use method from your Gin instance to add middleware to your router. In this middleware, you can include the Gin Logger and Recovery functions to handle logging and panics. Additionally, create an HTTP GET request for your API's homepage and set up a cookie store for your session, which you can then include in the Use method.

Implementing the Endpoint for Sign-up Request

To handle user requests for sign-up endpoints, add the following code to handler.go in handlers packages :

func (ga *GoApp) SignUp() gin.HandlerFunc {
    return func (ctx *gin.Context) {
        var user *model.User
        if err := ctx.ShouldBindJSON(&user); err != nil {
            _ = ctx.AbortWithError(http.StatusBadRequest, gin.Error{Err: err})
        }
        user.CreatedAt, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
        user.UpdatedAt, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
        user.Password, _ = encrypt.Hash(user.Password)
        if err := ga.App.Validate.Struct(&user); err != nil {
            if _, ok := err.(*validator.InvalidValidationError); !ok {
                _ = ctx.AbortWithError(http.StatusBadRequest, gin.Error{Err: err})
                ga.App.InfoLogger.Println(err)
                return
            }
        }

        ok, status, err := ga.DB.InsertUser(user)
        if err != nil {
            _ = ctx.AbortWithError(http.StatusInternalServerError, errors.New("error while adding new user"))
            ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
            return
        }
        if !ok {
            _ = ctx.AbortWithError(http.StatusInternalServerError, err)
            return
        }

        switch status {
        case 1:
            ctx.JSON(http.StatusOK, gin.H{"message": "Registered Successfully"})
            return
        case 2:
            ctx.JSON(http.StatusFound, gin.H{"message": "Existing Account, Go to the Login page"})
            return
        }
    }
}

Note that the handler method requires an HTTP request to the endpoint HTTP Header while you are running the server.

Let's talk about how you processed the user sign-up request. You created a variable referencing the user model struct and assumed the request contained data in the application/json format. Then, you bound the data to the user variable, checked for errors, and modified the password encryption. You validated the user struct tag values based on your input.

Now, you'll write queries to interact with your MongoDB database in the query package of the database.

Here's the InsertUser method implemented in the database package:


func (g *GoAppDB) InsertUser(user *model.User) (bool, int, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
    defer cancel()

    regMail := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
    ok := regMail.MatchString(user.Email)
    if !ok {
        g.App.ErrorLogger.Println("invalid registered details")
        return false, 0, errors.New("invalid registered details")
    }

    filter := bson.D{{Key: "email", Value: user.Email}}

    var res bson.M
    err := User(g.DB, "user").FindOne(ctx, filter).Decode(&res)
    if err != nil {
        if err == mongo.ErrNoDocuments {
            user.ID = primitive.NewObjectID()
            _, insertErr := User(g.DB, "user").InsertOne(ctx, user)
            if insertErr != nil {
                g.App.ErrorLogger.Fatalf("cannot add user to the database : %v ", insertErr)
            }
            return true, 1, nil
        }
        g.App.ErrorLogger.Fatal(err)
    }
    return true, 2, nil
}

The InsertUser method in the database query will add new users and assign them an ID or check if existing users are stored in the user collection by using the Email in your SignUp request.

Before searching the database, you validate the email with a regex expression.

You have successfully processed the SignUp endpoint and met all expected conditions to suit your needs.

To make the InsertUser method and other future methods available in the handlers package, you need to add them to the DBRepo interface as you did before.


package database

import "github.com/akinbyte/go-app/modules/model"

type DBRepo interface {
 InsertUser(user *model.User) (bool, int, error)
}

It would be best if you returned to the previous section to review the handler code. You need to add the method to the interface and call it using the DB field of the GoApp struct, passing the User struct model values as an argument.

After you receive the values, check for errors and use a switch-case statement to generate a JSON response with an appropriate HTTP response status code. This depends on the condition computed from the database query, which determines whether the user is registered.

For the SignUp handler to work, you will need the URL or path string of http://localhost:8080/sign-up to send an HTTP POST request. It's appropriate to use the HTTP POST method to indicate your desired action: sending data to the server for processing.

Setting the sign-up routes

After creating the handler for the sign-up endpoint, the next step is to set up a pathway to the endpoint for the user's request.


func Routes(r *gin.Engine, g *handlers.GoApp) {
 // .........
 // The comment above is to avoid repeating the previous code
 router.POST("/sign-up", g.SignUp())
}

Next, you can test the sign-up endpoint you implemented using the URL.

Testing the API endpoints I

API Client Set up

To test the API endpoints, you will use Thunder Client, a VsCode extension that works with REST APIs, instead of Postman. Follow these steps to get started:

  1. To begin, you should search for and download the Thunder Client extension on vs code:

  1. To access the extension, you need to click on the Flash icon located in the primary right bar:

  2. After that, you can open the Thunder Client and create a new collection named GoApp:

  1. Select the collection menu, add the home and sign up as new requests:

  1. Input the URL for each request, select the HTTP method to use, and add the user data for the POST request:

Start Application Server

Now that you have set up the API client let's start the server and test each API endpoint individually.

Before starting the server, ensure you are in your go-app project's root directory.

To start the server, use the command below in the terminal or command prompt:

For Mac or Linux users

//using wildcard syntax to execute all the files in the web directory
go run cmd/web/*.go

For Windows users

go run cmd/web/main.go routes.go middleware.go

Home Endpoint

As demonstrated above, sending an HTTP request to the home endpoint yields a successful response with a 200 status code and a JSON output.

Sign up Endpoint

By checking your user collection on MongoDB Atlas, you can confirm that the endpoint was successful and your user data has been added to the database.

Next, you will learn how to create an endpoint for handling user sign-in requests. You will use stored session cookies and implement JWT for authentication and authorization.

Implementing the Endpoint for Sign-in Request

You are almost done with your API endpoints and can now concentrate on creating the sign-in/login endpoint and its supporting internal processes. You will implement the following internal processes that were previously discussed in your package meetings in the following order:

  1. Generation and Parsing of JSON Web Tokens (JWT).

  2. Database Query for verifying user information.

  3. Develop a handler for the sign-in endpoint.

  4. Setting up the route for logging in.

This sequence will ensure that the necessary steps are taken to guarantee the security and integrity of the application.

Generation and Parsing of JSON Web Tokens (JWT)

What is JWT, and what does it use?

JWT is a secure and concise technique for transmitting information online. It shares data as a self-contained JSON object, signed with private or public keys for authenticity. The token includes three parts: the payload, header, and signature.

  • The payload contains user information and the token expiration date.

  • The header specifies the algorithms for creating the token's signature.

  • The signature results from encrypting the header and payload utilizing a secret key.

To begin with, you will install the jwt-go open-source library before delving into generating and parsing tokens effectively. To do this, follow the standard procedure and enter the command in the terminal.

go get github.com/golang-jwt/jwt/v4

Following this process, you will have installed the library on your machine and ensured it is correctly added to the go.mod that manages your dependencies.


package auth

import (
    "github.com/golang-jwt/jwt/v4"
    "github.com/akinbyte/go-app/modules/config"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "net/http"
    "os"
    "time"
)

var app config.GoAppTools

type GoAppClaims struct {
    jwt.RegisteredClaims
    Email string
    ID    primitive.ObjectID
}

var secretKey = os.Getenv("GOAPP_KEY")

func Generate(email string, id primitive.ObjectID) (string, string, error) {
    goappClaims := GoAppClaims{
        RegisteredClaims: jwt.RegisteredClaims{
            Issuer:   "goAppUser",
            IssuedAt: jwt.NewNumericDate(time.Now()),
            ExpiresAt: &jwt.NumericDate{
                Time: time.Now().Add(24 * time.Hour),
            },
        },
        Email: email,
        ID:    id,
    }

    newGoAppClaims := &jwt.RegisteredClaims{
        Issuer:   "goAppUser",
        IssuedAt: jwt.NewNumericDate(time.Now()),
        ExpiresAt: &jwt.NumericDate{
            Time: time.Now().Add(48 * time.Hour),
        },
    }
    token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, goappClaims).SignedString([]byte(secretKey))
    if err != nil {
        return "", "", err
    }
    newToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, newGoAppClaims).SignedString([]byte(secretKey))
    if err != nil {
        return "", "", err
    }
    return token, newToken, nil
}


func Parse(tokenString string) (*GoAppClaims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &GoAppClaims{}, func(t *jwt.Token) (interface{}, error) {
        return []byte(secretKey), nil
    })
    if err != nil {
        app.ErrorLogger.Fatalf("error while parsing token with it claims %v", err)
    }
    claims, ok := token.Claims.(*GoAppClaims)
    if !ok {
        app.ErrorLogger.Fatalf("error %v controller not authorised access", http.StatusUnauthorized)
    }
    if err := claims.Valid(); err != nil {
        app.ErrorLogger.Fatalf("error %v %s", http.StatusUnauthorized, err)
    }
    return claims, nil
}

I will simplify the code by explaining each step in detail.

  1. You create a struct called GoAppClaims, which includes a standard struct called RegisteredClaims and additional fields for email and a unique user ID.

  2. You define a global variable called secretKey, which you use to sign and verify tokens.

  3. The Generate function takes two input parameters, email and ID, and returns two JWTs as strings along with an error. It creates an instance of GoAppClaims, sets some claims, and signs the JWTs using secretKey.

  4. The newGoAppClaims instance is another instance of the RegisteredClaims struct with a longer validity period.

  5. You define the Parse function, which takes a tokenString input and returns a pointer to the GoAppClaims struct and an error. It uses ParseWithClaims to verify the signature using secretKey.

  6. To log any errors during token parsing, authorization, and validation, you use an instance of the built-in log package created in the config package.

Database Query for Verifying User Information

To ensure a successful sign-in process, users must be verified through the database and their details confirmed when accessing authorized endpoints.


func (g *GoAppDB) VerifyUser(email string) (primitive.M, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
    defer cancel()

    var res bson.M

    filter := bson.D{{Key: "email", Value: email}}
    err := User(g.DB, "user").FindOne(ctx, filter).Decode(&res)
    if err != nil {
        if err == mongo.ErrNoDocuments {
            g.App.ErrorLogger.Println("no document found for this query")
            return nil, err
        }
        g.App.ErrorLogger.Fatalf("cannot execute the database query perfectly : %v ", err)
    }

    return res, nil
}
  1. You have a VerifyUser method that searches the database for a document with the given email address. It takes in an email parameter and returns a BSON document (primitive.M) and an error.

  2. To maintain the request's lifecycle, you can set the Context timeout. Create an empty variable named res of type bson.M to store the query result. The email parameter is used as a filter to search the database.

  3. You can use the FindOne method on the user collection to get a single document result since you are only interested in a single user. This document is unmarshalled into the empty res variable using the Decode method

  4. If the document cannot be found, the query may return an error of mongo.ErrNoDocument. Otherwise, any other error is logged.

  5. If the query is executed successfully within the set timeout context, the method returns the result (res) and a nil error.

One of your tasks is to update the user collection in the database. You need to add or modify the JWT tokens found in the given query:


func (g *GoAppDB) UpdateInfo(userID primitive.ObjectID, tk map[string]string) (bool, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
    defer cancel()

    filter := bson.D{{Key: "_id", Value: userID}}
    update := bson.D{{Key: "$set", Value: bson.D{{Key: "token", Value: tk["t1"]}, {Key: "new_token", Value: tk["t2"]}}}}

    _, err := User(g.DB, "user").UpdateOne(ctx, filter, update)
    if err != nil {
        return false, err
    }
    return true, nil
}
  • To update the user information, you implement an UpdateInfo method, which is designed to update user information in the database. This method takes two parameters: UserID (a unique ID) and tk which holds generated tokens.

  • The UpdateInfo method returns a boolean value and an error.

  • First, you set a context timeout to pass the cancellation signal and stop any processes within the method.

  • You declare a filter variable assigned to a BSON document with a key-value pair of UserID to filter the user collection and the value to update to an update variable.

  • Next, you call the UpdateOne method on the user collection to make and pass in the filter and update variables as parameters to the do necessary updates.

  • After that, you handle the errors and return true if the update is successful or false if it fails.

Updated DB Interface

To access endpoints, update the DBRepo interface with a new query method. Then, Let’s update the database interface with newly implemented queries:


package database

import (
    "github.com/akinbyte/go-app/modules/model"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

type DBRepo interface {
    InsertUser(user *model.User) (bool, int, error)
    VerifyUser(email string) (primitive.M, error)
    UpdateInfo(userID primitive.ObjectID, tk map[string]string) (bool, error)
}

Development of a Handler for the sign-in endpoint

Now, you will assemble the code to handle your user sign-in requests and secure access:


func (ga *GoApp) SignIn() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        var user *model.User
        if err := ctx.ShouldBindJSON(&user); err != nil {
            _ = ctx.AbortWithError(http.StatusBadRequest, gin.Error{Err: err})
        }

        regMail := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
        ok := regMail.MatchString(user.Email)

        if ok {

            res, checkErr := ga.DB.VerifyUser(user.Email)

            if checkErr != nil {
                _ = ctx.AbortWithError(http.StatusUnauthorized, fmt.Errorf("unregistered user %v", checkErr))
                ctx.JSON(http.StatusUnauthorized, gin.H{"message": "unregistered user"})
                return
            }

            id := (res["_id"]).(primitive.ObjectID)
            password := (res["password"]).(string)

            verified, err := encrypt.Verify(user.Password, password)
            if err != nil {
                _ = ctx.AbortWithError(http.StatusUnauthorized, errors.New("cannot verify user details"))
                ctx.JSON(http.StatusUnauthorized, gin.H{"message": "Incorrect login details"})
                return
            }
            switch {
            case verified:
                cookieData := sessions.Default(ctx)

                userInfo := map[string]interface{}{
                    "ID":       id,
                    "email":    user.Email,
                    "password": user.Password,
                }
                cookieData.Set("data", userInfo)

                if err := cookieData.Save(); err != nil {
                    log.Println("error from the session storage")
                    _ = ctx.AbortWithError(http.StatusNotFound, gin.Error{Err: err})
                    return
                }
                // generate the jwt token
                t1, t2, err := auth.Generate(user.Email, id)
                if err != nil {
                    _ = ctx.AbortWithError(http.StatusInternalServerError, fmt.Errorf("token no generated : %v ", err))
                }

                cookieData.Set("token", t1)

                if err := cookieData.Save(); err != nil {
                    log.Println("error from the session storage")
                    _ = ctx.AbortWithError(http.StatusNotFound, gin.Error{Err: err})
                    return
                }

                // var tk map[string]string
                tk := map[string]string{"t1": t1, "t2": t2}

                // update the database, adding the token to user database

                _, updateErr := ga.DB.UpdateInfo(id, tk)
                if updateErr != nil {
                    _ = ctx.AbortWithError(http.StatusBadRequest, fmt.Errorf("unregistered user %v", updateErr))
                    ctx.JSON(http.StatusBadRequest, gin.H{"message": "Incorrect login details"})
                    return
                }

                ctx.JSON(http.StatusOK, gin.H{
                    "message":       "Successfully Logged in",
                    "email":         user.Email,
                    "id":            id,
                    "session_token": t1,
                })
            case !verified:
                ctx.JSON(http.StatusUnauthorized, gin.H{"message": "Incorrect login details"})
                return
            }

        }
    }
}

You create a variable called user to store JSON data from the user. You verify their email and details in the database and check their password using the Verify function. If verification is successful, you store the data as cookies. Otherwise, you return a JSON response.

Afterwards, you generate a JWT token using the Generate function and save it as a cookie for your custom middleware. You update the user collection with the token and send a JSON response with the status code. You prioritize error handling during every function call.

To send an HTTP POST request, you will need a URL or a path string of http://localhost:8080/sign-in.

Managing Cookies Data

In Go, to manage the transfer of data (cookies in our case) over the network, serialization technique is employed, which helps encode the stored data's datatypes. For this to be done, you need to use the built-in encoding/gob package, which manages streams of gobs - binary values exchanged between an Encoder (transmitter) and a Decoder (receiver). A stream of gobs is self-describing. Each data item in the stream is preceded by a specification of its type, expressed in a small set of predefined types.

Setting up the route for logging in.

In the main package's routes.go file, you should include the HTTP POST method route to handle sign-in endpoints:


 //............
//Post request to sign in 
router.POST("/sign-in", g.SignIn())

Testing the API endpoints II

You can test the API for the sign-in endpoint using the Thunder Client extension to see if all the implemented procedures are set up correctly.

Excellent, your sign-in endpoint is working as expected! You can be confident that it's free of error and that all the computed functions and processes within are working perfectly.

Implementing a Secure Endpoint

Create a custom middleware before adding a secure endpoint for authorized users. The Sign in endpoint is working well, so you can move forward.

Authorization and Authentication Middleware

We use a JWT token and middleware to give users access to a protected area. Middleware acts as a connector, helping applications work together and share data.

After you log in through the API, we use middleware to let you access secure areas. It handles the processing and communication needed for this.

package main

import (
    "net/http"

    "github.com/gin-contrib/sessions"
    "github.com/akinbyte/go-app/auth"

    "github.com/gin-gonic/gin"
    "github.com/pkg/errors"
)

func Authorization() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        cookieData := sessions.Default(ctx)
        tokenString := cookieData.Get("token").(string)
        if tokenString == "" {
            _ = ctx.AbortWithError(http.StatusNoContent, errors.New("no value for token"))
            return
        }

        parse, err := auth.Parse(tokenString)
        if err != nil {
            _ = ctx.AbortWithError(http.StatusUnauthorized, gin.Error{Err: err})
        }
        ctx.Set("pass", tokenString)
        ctx.Set("id", parse.ID)
        ctx.Set("email", parse.Email)
        ctx.Next()
    }
}

Your Authorization function, which you use as middleware, will check whether you are authorized to access a protected area. It will use a token from a cookie and check if it exists in the session cookie. If it doesn't exist, you will receive an error message. However, if the token is present, the function will set your ID, email, and token in the context of later functions.

Dashboard Endpoint Handler

To establish a default Dashboard handler that generates a primary JSON response to check the protected dashboard endpoint. You can create a sample code similar to that below:


func (ga *GoApp) DashBoard() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        ctx.JSON(http.StatusOK, gin.H{"resp": "Welcome to Go App Dashboard"})
    }
}

You can now add the dashboard endpoint routes to the main package's routes.go file, which will be the final update.

To send an HTTP GET request for this handler, you will need a URL or a path string of http://localhost:8080/auth/dashboard.

The code for this can be found below:


package main

import (
    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/gin"
    "github.com/akinbyte/go-app/handlers"
)

func Routes(r *gin.Engine, g *handlers.GoApp) {
    router := r.Use(gin.Logger(), gin.Recovery())

    router.GET("/", g.Home())

    // set up for storing details as cookies
    cookieData := cookie.NewStore([]byte("go-app"))
    router.Use(sessions.Sessions("session", cookieData))

    router.POST("/sign-up", g.SignUp())
    router.POST("/sign-in", g.SignIn())

    authRouter := r.Group("/auth", Authorization())
    {
        authRouter.GET("/dashboard")
    }
}

After adding the new code, you now employ the Group method to construct a fresh router group in which you can add all of the routes that share common middleware or have the same path prefix. You've now protected the dashboard endpoint using' Authorization' middleware.

Testing the dashboard endpoint

Now, You can test the secured endpoint to check if you have access to it after signing in.

Yeah! The secured endpoint (dashboard) worked, and you can now access it.

Conclusion

To summarise, you set up handlers/controllers for user requests, developed Routes to define the endpoints, implemented sign-up and sign-in requests, and established a secure dashboard endpoint using a custom middleware that authorizes user access through JWT token claims.

With this whole information piece, You should understand the basic concept and the complete basics of creating a well-structured REST API.

You can get the GitHub repo of the whole tutorial here.