Handler Forms Example

Demonstrates handler forms and 3 response methods in Lokstra

Related to: Key Features - Handler Forms


πŸ“– What This Example Shows

Handler Forms:

Response Methods (3 Ways):

  1. ApiHelper (opiniated, default for simple functions)
    • ctx.Api.Ok(), ctx.Api.Created(), ctx.Api.Error(), etc.
    • Wraps data in standard JSON structure
    • Can be customized if API response format differs
  2. response.Response (generic helper, not opiniated)
    • resp.Json(), resp.Html(), resp.Text(), resp.Stream()
    • Full control over response format
    • Custom status codes and headers
  3. Manual (http.ResponseWriter)
    • Direct ctx.W.Write() for maximum control
    • For special cases or custom protocols

πŸš€ Run the Example

# From this directory
go run main.go

Server will start on http://localhost:3001


πŸ§ͺ Test the Endpoints

Use the test.http file in VS Code with REST Client extension.

Simple Forms (Auto ApiHelper)

Simple Forms (Auto ApiHelper)

# Return string (auto wrapped)
curl http://localhost:3001/ping

# Return map (auto wrapped)
curl http://localhost:3001/time

# Return slice (auto wrapped)
curl http://localhost:3001/users

Request Binding

# Create user (JSON binding + validation)
curl -X POST http://localhost:3001/users \
  -H "Content-Type: application/json" \
  -d '{"name":"John","email":"john@example.com"}'

# Get user by ID (path param binding)
curl http://localhost:3001/users/123

# Update user (context + binding)
curl -X PUT http://localhost:3001/users/456 \
  -H "Content-Type: application/json" \
  -d '{"name":"Jane","email":"jane@example.com"}'

# Header binding
curl http://localhost:3001/headers \
  -H "Authorization: Bearer my-token" \
  -H "X-Custom-Header: custom-value"

ApiHelper Responses (Opiniated)

# Standard success
curl http://localhost:3001/api-ok

# Success with message
curl http://localhost:3001/api-ok-message

# Created (201)
curl -X POST http://localhost:3001/api-created \
  -H "Content-Type: application/json" \
  -d '{"name":"test"}'

# Error responses
curl http://localhost:3001/api-not-found
curl http://localhost:3001/api-bad-request

response.Response (Generic, Not Opiniated)

# JSON response
curl http://localhost:3001/resp-json

# HTML response
curl http://localhost:3001/resp-html

# Text response
curl http://localhost:3001/resp-text

# Custom status (202)
curl -X POST http://localhost:3001/resp-custom-status

# File download (stream)
curl http://localhost:3001/resp-download

Manual Responses (http.ResponseWriter)

# Manual JSON
curl http://localhost:3001/manual-json

# Manual with custom headers
curl -i http://localhost:3001/manual-custom

# Manual text
curl http://localhost:3001/manual-text

Error Handling

# Auto 500 error
curl http://localhost:3001/error-500

# Validation error (missing fields)
curl -X POST http://localhost:3001/validate \
  -H "Content-Type: application/json" \
  -d '{}'

# Validation success
curl -X POST http://localhost:3001/validate \
  -H "Content-Type: application/json" \
  -d '{"name":"Test","email":"test@example.com"}'

οΏ½ Response Examples

ApiHelper (Default, Opiniated)

Request: GET /ping

Response:

{
  "status": "success",
  "data": "pong"
}

Request: GET /api-ok-message

Response:

{
  "status": "success",
  "message": "Operation completed successfully",
  "data": {
    "status": "completed"
  }
}

response.Response (Generic)

Request: GET /resp-json

Response:

{
  "message": "Generic JSON response",
  "type": "custom"
}

Note: No wrapper - you control exact JSON structure

Manual Response

Request: GET /manual-custom

Response Headers:

HTTP/1.1 200 OK
Content-Type: application/json
X-Custom-Header: custom-value
X-Request-ID: req-123

Response Body:

{
  "message": "Manual response with custom headers"
}

πŸ’‘ Key Concepts

1. Three Response Methods

r.GET("/users", func(ctx *request.Context) error {
    return ctx.Api.Ok(users)  // Auto-wrapped in standard format
})

When to use:

Output:

{
  "status": "success",
  "data": {...}
}

Method 2: response.Response (For Custom Formats)

r.GET("/custom", func() (*response.Response, error) {
    resp := response.NewResponse()
    resp.Json(customData)  // Your exact JSON structure
    return resp, nil
})

When to use:

Output: Whatever you define

Method 3: Manual (For Special Cases)

r.GET("/special", func(ctx *request.Context) error {
    ctx.W.Header().Set("Content-Type", "application/xml")
    ctx.W.Write([]byte("<root>...</root>"))
    return nil
})

When to use:

2. Handler Forms by Complexity

Level 1: Simple (Auto ApiHelper)

// Return value - auto wrapped
r.GET("/ping", func() string {
    return "pong"  // β†’ {"status":"success","data":"pong"}
})

Level 2: With Error Handling

// Return value + error
r.GET("/users", func() ([]User, error) {
    users, err := db.GetAll()
    if err != nil {
        return nil, err  // β†’ 500 error
    }
    return users, nil  // β†’ {"status":"success","data":[...]}
})

Level 3: Request Binding

IMPORTANT: Request binding requires explicit struct with tags. You cannot use map[string]any directly.

// ❌ WRONG - Cannot bind directly to map
r.POST("/users", func(ctx *request.Context, req map[string]any) error {
    // This will NOT work!
})

// βœ… CORRECT - Must use struct with tags
type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

r.POST("/users", func(req *CreateUserRequest) (*User, error) {
    return createUser(req)  // Validation auto-handled
})

// βœ… CORRECT - Bind entire JSON body to map
type ApiCreatedParams struct {
    Data map[string]any `json:"*"` // `json:"*"` binds entire body
}

r.POST("/api-created", func(ctx *request.Context, req *ApiCreatedParams) error {
    return ctx.Api.Created(map[string]any{
        "id":   123,
        "data": req.Data, // Access via struct field
    }, "Resource created successfully")
})

// βœ… CORRECT - Bind from HTTP headers
type HeaderParams struct {
    Authorization string `header:"Authorization"`
    UserAgent     string `header:"User-Agent"`
    CustomHeader  string `header:"X-Custom-Header"`
}

r.GET("/headers", func(req *HeaderParams) (map[string]any, error) {
    return map[string]any{
        "authorization": req.Authorization,
        "user_agent":    req.UserAgent,
        "custom_header": req.CustomHeader,
    }, nil
})

Supported Binding Tags:

Key Rules:

Level 4: Full Control with Context

// Access everything + choose response method
r.GET("/custom", func(ctx *request.Context) error {
    // Option A: Use ApiHelper
    return ctx.Api.Ok(data)
    
    // Option B: Use response.Response
    resp := response.NewResponse()
    resp.Html("<h1>Hello</h1>")
    // ... manually write response
    
    // Option C: Manual
    ctx.W.Write([]byte("..."))
    return nil
})

3. Error Handling

Auto 500 (Return Error)

r.GET("/error", func() (string, error) {
    return "", fmt.Errorf("oops")  // β†’ 500 Internal Server Error
})

Custom Error (ApiHelper)

r.GET("/not-found", func(ctx *request.Context) error {
    return ctx.Api.Error(404, "NOT_FOUND", "Resource not found")
    // β†’ 404 with standard error format
})

Validation Errors (Auto)

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

r.POST("/users", func(req *CreateUserRequest) (*User, error) {
    // If validation fails β†’ Auto 400 with field errors
    return createUser(req)
})

🎯 Choosing Response Method

Scenario Method Why
REST API ApiHelper Consistent format, easy to use
Custom JSON response.Response Full control over structure
HTML pages response.Response .Html() method
File download response.Response .Stream() method
Binary data Manual Direct write
WebSocket Manual Protocol upgrade

πŸ” What’s Next?

Try modifying:

See more examples:


Questions? Check the Key Features Guide