Skip to content

Goten with Gin

dnahilman edited this page May 26, 2026 · 1 revision

Goten with Gin

This page is short by design: Goten exposes a plain http.Handler, and Gin wraps any http.Handler with gin.WrapH. No adapter package needed.

If you have not yet, walk through the Quick Start first to get Goten configured, the database migrated, and auth working with net/http. Then come back here to mount it under Gin.


1. Setup

On top of the Quick Start dependencies:

go get github.com/gin-gonic/gin

2. Mount Goten's auth endpoints

import (
    goten "github.com/dnahilman/goten"
    "github.com/gin-gonic/gin"
)

r := gin.Default()
r.Any("/api/auth/*action", gin.WrapH(auth.Handler()))

How this works: Goten internally registers exact routes like POST /api/auth/sign-up/email, GET /api/auth/get-session, and so on inside its own http.ServeMux. Gin's *action is a catch-all that forwards the full request — Goten then matches it against its registered routes. The wildcard path segment is captured by Gin but ignored; Goten reads r.URL.Path directly.

3. Protect your own routes

Two equivalent forms. Pick whichever reads better in your code.

Form A — one-liner with gin.WrapH

r.GET("/api/me", gin.WrapH(auth.RequireAuth(http.HandlerFunc(meHandler))))

func meHandler(w http.ResponseWriter, r *http.Request) {
    user, _ := goten.UserFromContext(r.Context())
    _ = json.NewEncoder(w).Encode(user)
}

Quick, but you give up the Gin handler signature.

Form B — Gin-idiomatic middleware adapter

// RequireAuth wraps Goten's auth middleware as a Gin middleware.
// It propagates the user+session that Goten injects into the request
// context back into c.Request, so downstream Gin handlers can read them.
func RequireAuth(auth *goten.Auth) gin.HandlerFunc {
    return func(c *gin.Context) {
        allow := false
        next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
            allow = true
            c.Request = r // carry the enriched context into Gin
        })
        auth.RequireAuth(next).ServeHTTP(c.Writer, c.Request)
        if !allow {
            c.Abort()
            return
        }
        c.Next()
    }
}

Then your route reads naturally:

r.GET("/api/me", RequireAuth(auth), func(c *gin.Context) {
    user, _ := goten.UserFromContext(c.Request.Context())
    c.JSON(http.StatusOK, user)
})

4. Reading the authenticated user / session

Both helpers take a context.Context and return (*Model, bool)not error. The bool is false when no user/session is in context (e.g. you forgot the middleware).

user, ok := goten.UserFromContext(c.Request.Context())
if !ok {
    // should not happen behind RequireAuth
    c.AbortWithStatus(http.StatusUnauthorized)
    return
}

session, _ := goten.SessionFromContext(c.Request.Context())
_ = session.ExpiresAt

5. Full runnable example

package main

import (
    "log"
    "net/http"

    goten "github.com/dnahilman/goten"
    gormadapter "github.com/dnahilman/goten/adapters/gorm"
    "github.com/gin-gonic/gin"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

func RequireAuth(auth *goten.Auth) gin.HandlerFunc {
    return func(c *gin.Context) {
        allow := false
        next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
            allow = true
            c.Request = r
        })
        auth.RequireAuth(next).ServeHTTP(c.Writer, c.Request)
        if !allow {
            c.Abort()
            return
        }
        c.Next()
    }
}

func main() {
    db, err := gorm.Open(
        postgres.Open("postgres://goten:goten@localhost:5432/goten?sslmode=disable"),
        &gorm.Config{},
    )
    if err != nil {
        log.Fatal(err)
    }

    auth, err := goten.New(goten.Config{
        BaseURL: "http://localhost:8080",
        Secret:  "your-32-byte-secret-key-here!!!!",
        Adapter: gormadapter.New(db),
    })
    if err != nil {
        log.Fatal(err)
    }

    r := gin.Default()

    // Goten's auth endpoints under /api/auth/*
    r.Any("/api/auth/*action", gin.WrapH(auth.Handler()))

    // Public route
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

    // Protected route
    r.GET("/api/me", RequireAuth(auth), func(c *gin.Context) {
        user, _ := goten.UserFromContext(c.Request.Context())
        c.JSON(http.StatusOK, user)
    })

    log.Fatal(r.Run(":8080"))
}

6. Testing the flow

The HTTP-level auth flow is identical to the Quick Start — r.Any("/api/auth/*action", ...) does not change request/response shapes. Use the curl walkthrough from Quick Start §8 to verify sign-up, sign-in, and protected routes.


Notes

  • No special adapter package. This page is everything. If you find yourself writing more glue than gin.WrapH and the optional middleware in §3 above, double-check that you are using the right http.Handler from Goten.
  • Other frameworks (Echo, Chi, Fiber). Same principle: any framework that can mount an http.Handler can host Goten. The Gin-specific bits here are just gin.WrapH and the gin.HandlerFunc adapter; substitute the equivalents for your framework.