Quick Start
This walks through a small user API with a login endpoint, protected routes, an admin-only route, and interactive documentation. Everything fits in one file.
Create the app
ezz.New() returns an app with recovery, request ID, and request logging middleware already installed. Auth registers the provider that turns an incoming request into a user, and after that every route requires a valid token until you say otherwise.
package main
import (
"log"
"time"
"github.com/sulv-io/ezz"
"github.com/sulv-io/ezz/auth"
"github.com/sulv-io/ezz/openapi"
)
var jwt = auth.NewHMACProvider("dev-secret-change-me", "quickstart")
func main() {
app := ezz.New().Auth(auth.NewJWTAuthProvider(jwt))
// routes go here
log.Fatal(app.Listen(":8080"))
}
Describe your requests and responses
Request types carry the binding rules in their tags. Response types are plain structs; ezz serializes whatever you return.
type LoginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
}
type LoginResponse struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
}
type GetUserRequest struct {
ID string `path:"id" validate:"required" description:"User ID"`
}
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"`
}
Add a public login route
.Public() opts a route out of authentication, which you need for login. Roles you want to check later go into the token's custom claims.
app.POST("/login", ezz.H(func(ctx *ezz.Context, req LoginRequest) (*LoginResponse, error) {
user, ok := authenticate(req.Email, req.Password)
if !ok {
return nil, ezz.Unauthorized("invalid email or password")
}
expires := time.Now().Add(24 * time.Hour)
token, err := jwt.GenerateToken(&auth.Claims{
Subject: user.ID,
ExpiresAt: expires.Unix(),
Custom: map[string]any{"role": user.Role},
})
if err != nil {
return nil, ezz.InternalServerError("could not issue token")
}
return &LoginResponse{Token: token, ExpiresAt: expires}, nil
})).Public().Tags("Auth")
Group the protected routes
A group shares a path prefix, and optionally middleware and an auth rule. Routes inside it inherit the app default, which is "any valid token".
api := app.Group("/api/v1")
api.GET("/me", ezz.HFunc(func(ctx *ezz.Context) (*User, error) {
user := ctx.User().(auth.User)
return lookup(user.ID())
})).Tags("Users")
api.GET("/users/{id}", ezz.H(func(ctx *ezz.Context, req GetUserRequest) (*User, error) {
user, ok := store[req.ID]
if !ok {
return nil, ezz.NotFound("user not found")
}
return user, nil
})).Tags("Users")
Restrict a route to admins
Auth rules can be set per route with .Auth(...) or on a whole group. ezz.Role checks the user value returned by your provider, which for JWT tokens reads the role and roles claims unless you configure a resolver.
admin := api.Group("/admin").Auth(ezz.Role("admin"))
admin.DELETE("/users/{id}", ezz.H(func(ctx *ezz.Context, req GetUserRequest) (*User, error) {
user, ok := store[req.ID]
if !ok {
return nil, ezz.NotFound("user not found")
}
delete(store, req.ID)
return user, nil
})).Tags("Users", "Admin")
Serve the documentation
This registers /openapi.json and a Scalar UI at /docs, both public.
openapi.SetupDocsEndpoints(app, openapi.ScalarConfig{
Title: "Quick Start API",
Layout: "modern",
DarkMode: true,
})
Try it
# no token, so this is rejected
curl -i localhost:8080/api/v1/me
# log in and keep the token
TOKEN=$(curl -s localhost:8080/login \
-H 'Content-Type: application/json' \
-d '{"email":"ada@example.com","password":"password123"}' | jq -r .token)
# now the protected route answers
curl -s localhost:8080/api/v1/me -H "Authorization: Bearer $TOKEN"
# validation runs before your handler
curl -s localhost:8080/login \
-H 'Content-Type: application/json' \
-d '{"email":"nope","password":"short"}'
The last call returns a 400 and a body like:
{
"error": true,
"message": "field 'Email' must be a valid email; field 'Password' must be at least 8",
"status": 400
}
Open http://localhost:8080/docs and you will find every route, with path parameters, request bodies, response schemas, and the min/max/email constraints carried over from the validate tags.
What to read next
| If you want to | Read |
|---|---|
| Understand patterns, groups, and route metadata | Routing |
| Bind headers, forms, or repeated query parameters | Request binding |
| Load roles and permissions from your database | Roles and permissions |
| Put GoTrue behind your API | GoTrue proxy |
| Write tests against the app | Testing |