DEV Community

Cover image for Go migrate from-express
Shabd Saran
Shabd Saran

Posted on

Go migrate from-express

Background: why the switch?

NodeJS made backend programming simpler for programmers as it enables you to write services in JavaScript. While NodeJS is still relevant today, JavaScript as a programming language isn’t ideal to write. Microsoft helped the JavaScript community with the introduction of TypeScript but that didn’t solve the underlying problem: the code is still transpiled to JavaScript.

I can write an entire article on JavaScript and TypeScript. Drop a comment if you’d like me to do that. Moving on…

Source code

You can find all the code from this article here: GitHub

I highly suggest that you go through the code to better understand things. Use this article just for understanding the reasoning behind the decisions made in the repository.

Picking the right framework

The focus will be to write an HTTP server. ExpressJS is the most popular framework to build HTTP servers using JavaScript. The base assumption from here will be that you are already familiar with it, and are now trying to switch to Go. I am not going to focus on Go itself. The focus will be how to replicate the same ExpressJS structure in Go. Some familiarity with Go will be helpful.

Gin is one of the most popular frameworks in Golang to build HTTP servers. My general advice would be to go for the most popular framework and libraries when picking a language you’re not comfortable with yet. Once you have mastered the default configurations and setup then you can focus on optimization and picking the right framework for your use case.

Basic project setup and directory structure

A good project structure is always going to help you down the road as the project scales. Code management is dependent on the programming language itself. Here are a few must-read articles on Golang project structure standard conventions:

  1. Standard Package Layout: https://www.gobeyond.dev/standard-package-layout
  2. Golang standards - project layout: https://github.com/golang-standards/project-layout

Standard practices to structure Go code

Note that it is not a standard practice to put all the code inside the src directory in Go. The following directories are a must in almost all the projects:

cmd

This is the application's entrypoint. It’s the Go equivalent of index.js in JavaScript projects. You don’t want to put a lot of code here; just project-level structs/interfaces and basic initialization setup.

The cmd directory must not include any implementation logic. Note that the root directory is not allowed to have any external dependencies. It must only call/initiate the respective application packages.

pkg

This is where you define the public packages in your application. This is where you write the actual implementation logic. Packages inside the pkg directory are meant to be shared outside of your application; keep that in mind.

internal

This directory is similar to the pkg directory except it is only meant to be used by your application. This layout pattern is enforced by the Go compiler itself.

The internal directory may also have a pkg sub-directory to better segregate the code. The pkg sub-directory will contain packages that are shared amongst the internal packages.

vendor

This directory contains your application dependencies. It is an automatically managed directory so you don’t have to manually change anything here. All you need to do is execute go mod vendor to create and maintain this directory. The base assumption here is that you are using Go modules for managing dependencies.

Note that you will need to add -mod=vendor to the go build command for building the project.

configs

Your application configurations will go here. You are probably going to read most of the configurations from the environment variables. You may use this directory to extract configurations from the environment variables and store them in a struct.

Project structure example

cmd/
    __app_name__/
        init.go # entrypoint
pkg/
    __app_name__/
        users/
        auth/
internal/
    __app_name__/
        razorpay/
vendor/
    github.com/
    golang.org/
    google.golang.org/
configs/
    configs.go
build/
    deploy/
        Dockerfile
go.mod
go.sum
Enter fullscreen mode Exit fullscreen mode

Route structure and middleware

The main.go file inside the cmd/__app_name__ directory will be simple. Its sole purpose will be to initiate the project and setup things in order.

package main

import (
    "fmt"
    "github.com/saranshabd/migrate-from-express/cmd/mfe/http"
    "os"

    "github.com/gin-gonic/gin"
    "github.com/saranshabd/migrate-from-express/cmd/mfe/logger"
    "github.com/saranshabd/migrate-from-express/configs"
)

func main() {

    // Create a Gin server application with default configurations & middlewares
    app := gin.Default()

    // Load all the application HTTP endpoints
    http.InitRoutes(app)

    // Start listening to HTTP requests from the client
    logger.App.Info().Msgf("Firing it up on %d port number.", configs.Configs.Port)
    if err := app.Run(fmt.Sprintf(":%d", configs.Configs.Port)); err != nil {
        logger.App.Error().Err(err).Msg("Could not fire up Gin :/")
        os.Exit(1) // Kill the application
    }
}
Enter fullscreen mode Exit fullscreen mode

All the route handlers will go inside the internal/__app_name__/ directory, based on the directory structure discussed above.

internal/
    middlewares/
    __route_name__/
        routes.go
        handler1.go
        handler2.go
Enter fullscreen mode Exit fullscreen mode

Firstly, we must ensure that all the HTTP responses we send are by default JSON. We can create a simple middleware for this purpose.

package middlewares

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

func JSONResponse() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Content-Type", "application/json")
        c.Next()
    }
}
Enter fullscreen mode Exit fullscreen mode

Secondly, we need a place to import all the HTTP route handlers and map them to specific endpoints. We can place this piece of code inside the cmd directory since all we will do is important a bunch of handlers and map them to their respective endpoints.

package http

import (
    "github.com/gin-gonic/gin"
    "github.com/saranshabd/migrate-from-express/internal/mfe/middlewares"
)

func InitRoutes(app *gin.Engine) {
    // Ensure that all the HTTP responses are by-default in the JSON format
    app.Use(middlewares.JSONResponse())

    // Load all the application HTTP routes
    // ...
}
Enter fullscreen mode Exit fullscreen mode

That’s it! The router and middleware setup in Gin is similar to that in ExpressJS. We can call this InitRoutes function from the main.go file.

Extract request parameters from different sources

We can move on to creating a bunch of HTTP endpoints with the current project setup. The endpoints will primarily focus on extracting request parameters from different sources e.g. request body, query and URI. The request params will all be returned in the response object as is.

I have created a bunch of simple HTTP utility functions for better code readability. These functions don’t have anything to do with the tutorial so I won’t go into the detail. You can find all the source code on GitHub.

Go treats functions as first-class citizens so your HTTP setup is going to be just a bunch of functions. An HTTP handler in Gin takes a single argument: Gin context. Context is one of the fundamentals of Go. If you’re not already familiar with it then I’d suggest you take some time in understanding them before you continue.

Extract from request body

Go provides a beautiful “binding” package out of the box for basic validation. The binding module can be loosely compared with “Joi” package in NodeJS. The primary difference between the two is that binding is part of Go language itself so we don’t have to define our validation logic separately.

This is how a typical Go HTTP handler looks like:

type CreateExtractParamsArgs struct {
    Arg1 string `json:"arg1" binding:"required"`
    Arg2 int64  `json:"arg2" binding:"required"`
    Arg3 bool   `json:"arg3,omitempty"` // optional
}

func CreateExtractParams(c *gin.Context) {
    // Extract the parameters from the request.body & validate the object received
    var args CreateExtractParamsArgs
    if err := c.BindJSON(&args); err != nil {
        // Return HTTP 400 if invalid params are passed
        http.InvalidRequestParams(c)
        return
    }

    // Return the params object in response if everything is fine
    http.RespondOkay(c, "Request parameters extracted successfully", args)
}
Enter fullscreen mode Exit fullscreen mode

Note that I am using the custom http package, which is just a utility package I created. You can refer to the RespondOkay function below:

func RespondOkay(c *gin.Context, message string, data any) {
    Respond(c, http.StatusOK, true, message, data)
}

func Respond(c *gin.Context, statusCode int, success bool, message string, data any) {
    c.JSON(statusCode, &gin.H{
        "success": success,
        "message": message,
        "data":    data,
    })
}
Enter fullscreen mode Exit fullscreen mode

The omitempty property will remove the field while deserialising, to let’s say JSON. The struct value of the field will be the default value of the type. E.g. if you access args.Arg3 then you will receive false in the response. Beware of this behaviour. This is not JavaScript!

Extract from query parameters

We can use the same pattern to validate query parameters using the binding package. We will use the BindQuery function instead of BindJSON. Everything else will look the same.

type GetExtractParamsArgs struct {
    Arg1 string `form:"arg1" json:"arg1" binding:"required"`
    Arg2 int64  `form:"arg2" json:"arg2" binding:"required"`
    Arg3 bool   `form:"arg3" json:"arg3,omitempty"` // optional
}

func GetExtractParams(c *gin.Context) {
    // Extract the parameters from the request.query & validate the object received
    var args GetExtractParamsArgs
    if err := c.BindQuery(&args); err != nil {
        // Return HTTP 400 if invalid params are passed
        http.InvalidRequestParams(c)
        return
    }

    // Return the params object in response if everything if fine
    http.RespondOkay(c, "Request parameres extracted successfully", args)
}
Enter fullscreen mode Exit fullscreen mode

Extract from the URI

Again, we can just extend the binding package to validate path parameters i.e. /path/:arg/. You define the path parameters in Gin like this:

r.GET("/:specificPath/", GetSpecificExtractParams)
Enter fullscreen mode Exit fullscreen mode

We can swap the BindQuery function with BindUri and keep everything the same. By now, you must be getting the hang of Go.

type GetSpecificExtractParamsArgs struct {
    SpecificPath string `uri:"specificPath" json:"specificPath" binding:"required"`
}

func GetSpecificExtractParams(c *gin.Context) {
    // Extract the parameters from the request.params & validate the object received
    var args GetSpecificExtractParamsArgs
    if err := c.BindUri(&args); err != nil {
        // Return HTTP 400 if invalid params are passed
        http.InvalidRequestParams(c)
        return
    }

    // Return the params object in response if everything is fine
    http.RespondOkay(c, "Request parameters extracted successfully", args)
}
Enter fullscreen mode Exit fullscreen mode

Header-based authorization

Authentication and authorization are the base of almost all the REST APIs out there. Auth is usually built on top of HTTP headers. We can use the binding package to validate headers received in the request and write middlewares to add custom logic on top of that.

Here’s a simple example of a auth header to check the source of the incoming request:

type VerifiedSourcesArgs struct {
    VerifiedSource string `header:"verified-source" binding:"required"`
}

const (
    AcceptedSource = "accepted-source"
)

func IsVerifiedSource() gin.HandlerFunc {
    return func(c *gin.Context) {
        // Extract the header value from the HTTP request
        var args VerifiedSourcesArgs
        if err := c.ShouldBindHeader(&args); err != nil {
            http.InvalidHeaders(c)
            return
        }

        // Check if the source of the incoming request is accepted
        if args.VerifiedSource != AcceptedSource {
            http.InvalidHeaders(c)
            return
        }

        // Continue with the chain of middlewares/handlers
        c.Next()
    }
}
Enter fullscreen mode Exit fullscreen mode

The Next function in Gin middlewares

Gin middlewares look awfully similar to ExpressJS middlewares. But, there is a nasty distinction between the two; the Next function. You do not need to necessarily call the Next function in a Gin middleware.

This is an important distinction because you cannot expect a request to terminate just because you returned from the middleware without calling the Next function. The next in-chain function will still be called when your middleware terminates… unless you abort the request.

func AbortAndRespond(c *gin.Context, statusCode int, message string, data any) {
    c.AbortWithStatusJSON(statusCode, &gin.H{
        "success": false,
        "message": message,
        "data":    data,
    })
}
Enter fullscreen mode Exit fullscreen mode

Instead of calling c.JSON we need to call c.AbortWithStatusJSON function to set the status code and JSON response before aborting the request. This is a nasty distinction especially for programmers coming from the NodeJS background. Beware!


That's it! We setup HTTP routes, validated incoming request data from different sources, and created auth middlewares for the application.

This is just the beginning. The application didn't interact with a database or a third-party service; both are fundamentals to most of the services. That's for the next article in the series :)

Drop a comment if there's anything I missed or could've explained better.

Cheers! 👋

Top comments (0)