DEV Community

loading...
Cover image for 02 - Creating Route Handlers In Gin

02 - Creating Route Handlers In Gin

jacobsngoodwin profile image Jacob Goodwin Updated on ・5 min read

In the previous tutorial we set up a Go application called account with automatic reloading behind a Traefik reverse proxy, all running in a Docker development environment.

In this tutorial we'll set up http routes using gin, which will allow us to create API endpoints for managing a user's account.

By the time we're done, your file structure should be as follows.

Final File Structure

FYI - I've created a git branch for each tutorial in the Github repository. You can use the branch for tutorial 01 to start off with the same code I'm using today! To start the app, run docker-compose up inside of the top-level directory.

If you prefer, check out the video tutorial as well!

Adding Handler Package

To get started, let's create a new folder inside of ~/account called handler. Inside of this folder, create a file called handler.go. This file will be used to initialize the handler package. My convention in this project will be to name the the "main" file for each package with the name of the package.

We do the following in this file.

  1. Create a Handler struct. This struct will ultimately hold "services" properties. These services contain methods that implement the application's core functionality.
  2. We create a Config struct. This config will hold all values needed to initialize this handler package.
  3. A NewHandler "factory" function. This function is used to initialize the routes and will be called from the main package.

I will be using this factory with configuration approach throughout the project as it helps make clear which dependencies are required for each package. And while you will often see this in other Go projects, it is by no means standard. One can directly initialize a struct instead.

We will pass a reference to the gin router created inside of package main into the Config for our handler package. With this reference, we can create a route group which allows us to set a common "base URL" ("/api/account") for our endpoints.

On this route group, we'll create a get route which should return basic JSON. This GET route has been copied and pasted from the route we originally placed inside of main.go.

package handler

import (
    "net/http"

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

// Handler struct holds required services for handler to function
type Handler struct{}

// Config will hold services that will eventually be injected into this
// handler layer on handler initialization
type Config struct {
    R *gin.Engine
}

// NewHandler initializes the handler with required injected services along with http routes
// Does not return as it deals directly with a reference to the gin Engine
func NewHandler(c *Config) {
    // Create a handler (which will later have injected services)
  h := &Handler{} // currently has no properties

    // Create an account group
    g := c.R.Group("/api/account")

    g.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "hello": "space persons",
        })
    })
}

Before we test this code, make sure to remove the GET route handler from package main and replace it with the initialization of the newly created package handler. Note the NewHandler factory merely modifies the gin router and does not return anything.

router := gin.Default()

handler.NewHandler(&handler.Config{
  R: router,
})

srv := &http.Server{
  Addr:    ":8080",
  Handler: router,
}

Creating Application Routes

Inside handler.go, we'll add all of the routes inside of the NewHandler function along with corresponding handler methods for each route. We'll eventually move the handler functions to separate files as the application grows.

You can see that each handler method returns some simple json with "hello" as the key, and a value telling which handler has been called.

func NewHandler(c *Config) {
    // Create an account group
    // Create a handler (which will later have injected services)
    h := &Handler{} // currently has no properties

    // Create a group, or base url for all routes
    g := c.R.Group("/api/account")

    g.GET("/me", h.Me)
    g.POST("/signup", h.Signup)
    g.POST("/signin", h.Signin)
    g.POST("/signout", h.Signout)
    g.POST("/tokens", h.Tokens)
    g.POST("/image", h.Image)
    g.DELETE("/image", h.DeleteImage)
    g.PUT("/details", h.Details)
}

// Me handler calls services for getting
// a user's details
func (h *Handler) Me(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "hello": "it's me",
    })
}

// Signup handler
func (h *Handler) Signup(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "hello": "it's signup",
    })
}

// Signin handler
func (h *Handler) Signin(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "hello": "it's signin",
    })
}

// Signout handler
func (h *Handler) Signout(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "hello": "it's signout",
    })
}

// Tokens handler
func (h *Handler) Tokens(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "hello": "it's tokens",
    })
}

// Image handler
func (h *Handler) Image(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "hello": "it's image",
    })
}

// DeleteImage handler
func (h *Handler) DeleteImage(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "hello": "it's deleteImage",
    })
}

// Details handler
func (h *Handler) Details(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "hello": "it's details",
    })
}

With your docker-compose up running, you should now be able to make request to the above endpoints using the client of your choice. I use a lot of Postman, but here's a quick example with curl from the command line.

➜  curl -X GET http://malcorp.test/api/account/me
{"hello":"it's me"}%
➜  curl -X POST http://malcorp.test/api/account/signin
{"hello":"it's signin"}%

Configure Base URL with Environment Variable

We're going to add the base url /api/account to an environment variable. We'll do this now so that setting up environment variables later on will be a breeze!

To set project-wide environment variables, let's create a ".env.dev" file in the root memrizr directory.

ACCOUNT_API_URL=/api/account

I also recommend creating a .gitignore file and adding ".env.dev" as an entry so that this file is omitted from your repository (eventually we'll add some credentials you don't want ending up online).

We can then inject this environment variable into the account service defined in the docker-compose.yml file as follows. Now the account application will have access to any environment variable added to .env.dev! This is just one way to create environment variables in Docker or Docker Compose.

 account:
    ...
    ...
    env_file: .env.dev

Finally, we need to tell the application to use this environment variable inside of handler.go. To make this work, update the name of argument of Group to get the environment variable we just setup.

// Create a group, or base url for all routes
    g := c.R.Group(os.Getenv("ACCOUNT_API_URL"))

Now you can restart the application with docker-compose up, and re-run the above curl commands (or in the client of your choice).

Next Time

Now that we've created routes, we have an entry point from the outside world into our application. This prepares us to discuss the application architecture and to being building out the layers of the application.

¡Hasta el próximo tutorial!

Discussion (0)

pic
Editor guide