Example 04: Handler Forms

Explore 4 essential handler patterns
Time: 10 minutes โ€ข Concepts: Handler signatures, when to use each form


๐ŸŽฏ What Youโ€™ll Learn


๐Ÿš€ Run It

cd docs/01-router-guide/01-router/examples/04-handler-forms
go run main.go

Server starts on: http://localhost:3000


๐Ÿงช Test It

Form 1: Simple Return Value

curl http://localhost:3000/ping

Response:

"pong"
curl http://localhost:3000/time

Response:

{
  "current_time": "2025-10-22T10:30:00Z"
}

Form 2: Return with Error (Most Common!)

curl http://localhost:3000/users

Response:

[
  {"id": 1, "name": "Alice", "email": "alice@example.com"},
  {"id": 2, "name": "Bob", "email": "bob@example.com"}
]

Form 3: Request Binding with Error

curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Charlie","email":"charlie@example.com"}'

Response:

{
  "id": 3,
  "name": "Charlie",
  "email": "charlie@example.com"
}
curl http://localhost:3000/users/1

Response:

{
  "id": 1,
  "name": "Alice",
  "email": "alice@example.com"
}

Form 4: Context + Request with Error

curl http://localhost:3000/users/1/details -H "User-Agent: MyApp/1.0"

Server logs:

Request from: MyApp/1.0

Response:

{
  "id": 1,
  "name": "Alice",
  "email": "alice@example.com"
}

Form 5: Custom Response (Full Control)

curl -i http://localhost:3000/users/1/custom

Response (with custom headers):

HTTP/1.1 200 OK
Content-Type: application/json
X-User-ID: 1
X-Response-Time: 2025-10-22T10:30:00Z

{
  "id": 1,
  "name": "Alice",
  "email": "alice@example.com"
}

Error response:

curl -i http://localhost:3000/users/999/custom
HTTP/1.1 404 Not Found
X-Error-Code: USR404

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User does not exist"
  }
}

๐Ÿ“ Handler Forms Explained

Form 1: Simple Return Value

Signature: func() T

When to use:

Example:

r.GET("/ping", func() string {
    return "pong"
})

r.GET("/config", func() map[string]string {
    return map[string]string{
        "version": "1.0",
        "env": "production",
    }
})

Form 2: Return with Error (90% of Cases!)

Signature: func() (T, error)

When to use:

Example:

r.GET("/users", func() ([]User, error) {
    users, err := db.GetUsers()
    if err != nil {
        return nil, err  // Lokstra handles error response
    }
    return users, nil  // Auto 200 OK with JSON
})

Why most common?


Form 3: Request Binding with Error

Signature: func(req *RequestType) (T, error)

When to use:

Example:

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

r.POST("/users", func(req *CreateUserRequest) (*User, error) {
    // req is auto-bound from JSON body
    // auto-validated
    user, err := db.CreateUser(req.Name, req.Email)
    return user, err
})

Binding sources:


Form 4: Context + Request with Error

Signature: func(ctx *request.Context, req *RequestType) (T, error)

When to use:

Example:

r.GET("/users/{id}", func(ctx *request.Context, req *GetUserRequest) (*User, error) {
    // Access headers
    authToken := ctx.R.Header.Get("Authorization")
    userAgent := ctx.R.Header.Get("User-Agent")
    
    // Log request
    log.Printf("User %d requested by %s", req.ID, userAgent)
    
    // Still have request binding
    user, err := db.GetUser(req.ID)
    return user, err
})

What ctx provides:


Form 5: Custom Response (Full Control)

Signature: func(...) (*response.Response, error)

When to use:

Example:

r.GET("/users/{id}", func(ctx *request.Context, req *GetUserRequest) (*response.Response, error) {
    user, err := db.GetUser(req.ID)
    if err != nil {
        // Custom error response
        return response.Error(404, "USER_NOT_FOUND", "User does not exist").
            WithHeader("X-Error-ID", generateErrorID()), nil
    }
    
    // Custom success response
    return response.Success(user).
        WithHeader("X-User-ID", fmt.Sprintf("%d", user.ID)).
        WithHeader("Cache-Control", "max-age=3600").
        WithStatus(200), nil
})

๐Ÿ“Š Comparison Table

Form Parameters Return Use Case % Usage
1 None T Simple, static 5%
2 None (T, error) Can fail 60%
3 Request (T, error) Need input 30%
4 Context + Request (T, error) Need headers 4%
5 Context (optional) (*Response, error) Custom control 1%

Recommendation:


๐ŸŽ“ What You Learned


๐Ÿ’ก Decision Guide

Do you need request data (body/path/query)?
โ”œโ”€ NO  โ†’ Do errors happen?
โ”‚       โ”œโ”€ NO  โ†’ Form 1 (simple return)
โ”‚       โ””โ”€ YES โ†’ Form 2 (return with error)
โ”‚
โ””โ”€ YES โ†’ Do you need headers/cookies?
         โ”œโ”€ NO  โ†’ Form 3 (request binding)
         โ”œโ”€ YES โ†’ Do you need custom status/headers?
                  โ”œโ”€ NO  โ†’ Form 4 (context + request)
                  โ””โ”€ YES โ†’ Form 5 (custom response)

๐Ÿ”— Additional Forms

Lokstra supports 29 total handler forms!

Other useful forms:

See: Deep Dive: All 29 Handler Forms


Next: Try combining these patterns in a complete application!

Back: 03 - Route Groups