#Confusion in Gin API project structure

27 messages · Page 1 of 1 (latest)

naive tangle
#

I'm confused as to how to structure my project while using gin. I want it to be TDD-like. I followed the process but now I'm stuck.
I want it to be something like https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/http-handlers-revisited#new-design where my Server struct would have db connection, router, logging configs etc and I can swap them out while testing. I realise this will happen through the usage of interfaces but I'm still confused. I'm posting my code below. Please help. Any other tips adjacent to the topic in question is also hugely appreciated.

main.go

package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

func main() {
    r := gin.Default()
    r = SetupRoutes(r)
    r.Run()
}

func SetupRoutes(r *gin.Engine) *gin.Engine {
    r.Any("/health", GetHealthStatus)
    r.GET("/", GetHomePage)

    return r
}

func GetHealthStatus(ctx *gin.Context) {
    ctx.JSON(http.StatusOK, gin.H{
        "mssg": "OK",
    })
}

func GetHomePage(ctx *gin.Context) {
    ctx.String(http.StatusOK, "This is the homepage. HTML will be added later")
}

maintest.go

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
)

func TestServer(t *testing.T) {
    gin.SetMode(gin.TestMode)
    router := gin.Default()
    router = SetupRoutes(router)

    t.Run("test /health route", func(t *testing.T) {
        w := httptest.NewRecorder()
        r, _ := http.NewRequest(http.MethodGet, "/health", nil)

        router.ServeHTTP(w, r)

        assert.Equal(t, w.Code, http.StatusOK)
        assert.JSONEq(t, w.Body.String(), `{"mssg": "OK"}`)
    })

    t.Run("test / route", func(t *testing.T) {
        w := httptest.NewRecorder()
        r, _ := http.NewRequest(http.MethodGet, "/", nil)

        router.ServeHTTP(w, r)

        assert.Equal(t, w.Code, http.StatusOK)
    })
}

vapid yacht
#

I recommend moving code out of package main and function main() to keep it small. Put all app logic in a separate package.

vapid yacht
#

I also recommend that you use table driven tests so that you can test more cases for each endpoint, without having to duplicate the test code.

naive tangle
vapid yacht
#

ah

#

For setting up the routes, I would do something like this instead:

func RegisterRoutes(r gin.IRoutes) {
    r.GET("/health", GetHealthStatus)
    r.HEAD("/health", GetHealthStatus)

    r.GET("/", GetHomePage)
}
#

And then check the method in the health handler and respond with code 204, no content when the method is HEAD

#

And I wouldn't use gin.H, I would have a struct that's marshalled instead, like this:

type HealthStatus struct {
    Message string `json:"msg" xml:"msg"`
}
#
    ctx.JSON(http.StatusOK, HealthStatus{
        Message: HealthStatusOK,
    })
naive tangle
#

take a look at code snippet of main.go

#

I didn't made myself clear. I want to understand what types and interfaces needed for routing, db configs etc while making the server

vapid yacht
#

I think I get it now.

#

Well, the interface for your database depends on what you store.

#

So need more information on what you're actually storing.

#

There's no universal interface for database, logging etc

#

Do you have a database table schemas and queries?

#

For logging, I'd say, use log/slog package. It's pretty good.

#

No need for an interface there, since slog wants slog.Handler, which is an interface.

#

So if your app just accepts a *slog.Logger, then you can plug in any handler if you ever want to change the logging for various reasons, including testing.

vapid yacht
#

Does this address some of your questions?

naive tangle
#

Yes, it does.

#

For now, this is how I have structured my code.

main.go

package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

type Server struct {
    // db type here
    *gin.Engine        // router
}

func main() {
    s := NewServer(gin.Default())
    s.Run()
}

func NewServer(r *gin.Engine) *Server {
    server := new(Server)

    r.Any("/health", server.GetHealthStatus)
    r.GET("/", server.GetHomePage)
    server.Engine = r

    // new db conn function executed here

    return server
}

func(s *Server) GetHealthStatus(ctx *gin.Context) {
    ctx.JSON(http.StatusOK, gin.H{
        "mssg": "OK",
    })
}

func(s *Server) GetHomePage(ctx *gin.Context) {
    ctx.String(http.StatusOK, "This is the homepage. HTML will be added later")
}

maintest.go

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
)

func TestServer(t *testing.T) {
    gin.SetMode(gin.TestMode)
    router := gin.Default()
    server := NewServer(router)

    t.Run("test /health route", func(t *testing.T) {
        w := httptest.NewRecorder()
        r, _ := http.NewRequest(http.MethodGet, "/health", nil)

        server.ServeHTTP(w, r)

        assert.Equal(t, w.Code, http.StatusOK)
        assert.JSONEq(t, w.Body.String(), `{"mssg": "OK"}`)
    })

    t.Run("test / route", func(t *testing.T) {
        w := httptest.NewRecorder()
        r, _ := http.NewRequest(http.MethodGet, "/", nil)

        server.ServeHTTP(w, r)

        assert.Equal(t, w.Code, http.StatusOK)
    })
}

#

Thanks a lot

#

@vapid yacht

vapid yacht
#

You're welcome. 🙂

I recommend that you create an interface for your database, so that you can easily mock it or replace it with something else if that ever becomes a question. The benefit to testing it massive.