Guards: Authentication and Route Authorization in Go
A guard is a small piece of logic that decides whether a request is allowed to reach your handler. In NestGo, guards are the Go equivalent of NestJS guards — the standard place for authentication checks and route authorization in a Golang API.
Guards run before the handler (and before interceptors). If a guard denies the request, the handler never executes.
The Guard interface
A guard implements a single method:
// Guard decides whether a request is allowed to proceed.
// Return true to allow, false to deny.
// Returning an error short-circuits with that error.
type Guard interface {
CanActivate(c Context) (bool, error)
}
The return values control what happens next:
| Return | Result |
|---|---|
true, nil | Request proceeds to the next guard (or the handler) |
false, nil | Request is rejected with 403 Forbidden ("access denied") |
_, err | Chain short-circuits with err — use this to send a specific status like 401 |
Return an error (for example core.ErrUnauthorized(...)) when you want to control the status code and message; return false, nil when the generic 403 is fine.
GuardFunc: write a guard as a function
For most guards you don’t need a struct — GuardFunc adapts a plain function to the Guard interface:
authGuard := core.GuardFunc(func(c core.Context) (bool, error) {
token := c.GetHeader("Authorization")
if token == "" {
return false, core.ErrUnauthorized("missing token")
}
return true, nil
})
A struct-based guard is useful when the guard has dependencies (a token verifier, a user service):
type JWTGuard struct {
verifier TokenVerifier
}
func (g *JWTGuard) CanActivate(c core.Context) (bool, error) {
token := c.GetHeader("Authorization")
claims, err := g.verifier.Verify(token)
if err != nil {
return false, core.ErrUnauthorized("invalid token")
}
c.Set("user", claims) // downstream guards/handlers can read this
return true, nil
}
UseGuards: turn guards into middleware
UseGuards converts one or more guards into a MiddlewareFunc so they plug into any route, group, or the whole server:
func UseGuards(guards ...Guard) MiddlewareFunc
Guards run in the order given — the first failure stops the chain.
mw := core.UseGuards(authGuard, adminGuard) // auth first, then role check
How to apply guards at global, controller, and route level
NestGo supports the same three levels as NestJS:
// 1. Global — every route in the app:
config.GlobalGuards = []core.Guard{authGuard}
// 2. Controller / group level — every route in the group:
admin := r.Group("/admin", core.UseGuards(adminGuard))
// 3. Route level — one specific route:
r.DELETE("/:id", handler, core.UseGuards(ownerGuard))
| Level | How | Typical use |
|---|---|---|
| Global | config.GlobalGuards = []core.Guard{...} | Authentication for the whole API |
| Controller/group | r.Group(prefix, core.UseGuards(...)) | Admin-only sections |
| Route | r.DELETE("/:id", handler, core.UseGuards(...)) | Ownership or per-action checks |
In the full execution chain, guards sit between exception filters and pipes: Filters → Guards → Pipes → Interceptors → Handler. See Exception Filters for the complete picture, and RouteOptions for bundling all of them in the correct order.
Role-based authorization with route metadata
NestJS uses @SetMetadata('roles', ...) plus a Reflector; NestGo uses core.WithMeta, which stores a value in the request context so guards can read it with c.Get(key).
// A role guard that reads the route's "roles" metadata:
roleGuard := core.GuardFunc(func(c core.Context) (bool, error) {
meta := c.Get("roles")
if meta == nil {
return true, nil // route declares no role requirement
}
allowed, ok := meta.([]string)
if !ok {
return false, nil
}
userRole, _ := c.Get("role").(string) // set earlier by the auth guard
for _, r := range allowed {
if r == userRole {
return true, nil
}
}
return false, core.ErrForbidden("insufficient role")
})
// Attach metadata + guard to a route.
// WithMeta must come BEFORE UseGuards so the value is set when the guard runs:
r.DELETE("/:id", handler,
core.WithMeta("roles", []string{"admin"}),
core.UseGuards(roleGuard),
)
To attach several metadata keys at once, use core.Meta:
r.GET("/reports", handler,
core.Meta(map[string]interface{}{
"roles": []string{"admin", "auditor"},
"permission": "reports:read",
}),
core.UseGuards(roleGuard),
)
Complete example: auth + roles on an admin group
package main
import "github.com/ashrafAli23/nestgo/core"
func RegisterAdminRoutes(r core.Router) {
authGuard := core.GuardFunc(func(c core.Context) (bool, error) {
token := c.GetHeader("Authorization")
if token == "" {
return false, core.ErrUnauthorized("missing token")
}
// ...verify token, then store the user's role:
c.Set("role", "admin")
return true, nil
})
roleGuard := core.GuardFunc(func(c core.Context) (bool, error) {
allowed, _ := c.Get("roles").([]string)
userRole, _ := c.Get("role").(string)
for _, r := range allowed {
if r == userRole {
return true, nil
}
}
return false, nil // -> 403 "access denied"
})
admin := r.Group("/admin", core.UseGuards(authGuard))
admin.DELETE("/users/:id", deleteUserHandler,
core.WithMeta("roles", []string{"admin"}),
core.UseGuards(roleGuard),
)
}
Requests without a token get 401, authenticated users without the admin role get 403, and admins reach the handler.
Guards vs middleware in Go
Plain middleware can do everything a guard does — so why guards? Guards give authorization a name and a contract: CanActivate returns an explicit allow/deny, denial handling is centralized (403 by default), and RouteOptions slots guards into the correct position in the chain automatically. Use middleware for transport concerns (CORS, compression, logging) and guards for “may this request proceed?”.
Source: github.com/ashrafAli23/nestgo (core/guard.go, core/meta.go).
Next steps
- Interceptors — run logic before and after the handler
- Pipes — transform and validate extracted request values
- Exception Filters — customize error responses and see the full execution order