Example 05: Response Patterns

Master all 3 response methods and 2 response paths
Time: 15 minutes β€’ Concepts: Response types, paths, when to use each


🎯 What You’ll Learn

Lokstra provides flexibility in how you send responses:

3 Response Types:

  1. Manual - Full control using http.ResponseWriter
  2. Generic - Unopinionated using response.Response (JSON, HTML, text, etc)
  3. Opinionated - Structured API using response.ApiHelper (JSON only)

2 Response Paths:

  1. Via Context - Use request.Context to write response
  2. Via Return - Return data or response objects

πŸš€ Run It

cd docs/01-router-guide/01-router/examples/05-response-patterns
go run main.go

Server starts on: http://localhost:3000


πŸ“ Understanding Response Types

Type 1: Manual Response (http.ResponseWriter)

Full manual control - You write everything yourself.

r.GET("/manual/json", func(ctx *request.Context) error {
    ctx.W.Header().Set("Content-Type", "application/json")
    ctx.W.Header().Set("X-Custom-Header", "value")
    ctx.W.WriteHeader(200)
    ctx.W.Write([]byte(`{"message":"Manual response"}`))
    return nil
})

When to use:

Pros:

Cons:


Type 2: Generic Response (response.Response)

Unopinionated - Can send JSON, HTML, text, or any format.

// JSON
r.GET("/response/json", func() *response.Response {
    resp := response.NewResponse()
    resp.RespHeaders = map[string][]string{
        "X-Custom": {"value"},
    }
    resp.WithStatus(200).Json(data)
    return resp
})

// HTML
r.GET("/response/html", func() *response.Response {
    resp := response.NewResponse()
    resp.Html("<html>...</html>")
    return resp
})

// Plain Text
r.GET("/response/text", func() *response.Response {
    resp := response.NewResponse()
    resp.Text("Plain text")
    return resp
})

When to use:

Pros:

Cons:


Type 3: Opinionated API (response.ApiHelper)

Structured JSON API - Standard response format enforced.

// Success
r.GET("/api/success", func() *response.ApiHelper {
    api := response.NewApiHelper()
    api.Ok(data)  // Standard success format
    return api
})

// Success with message
r.GET("/api/message", func() *response.ApiHelper {
    api := response.NewApiHelper()
    api.OkWithMessage(data, "Operation successful")
    return api
})

// Created (201)
r.POST("/api/created", func() *response.ApiHelper {
    api := response.NewApiHelper()
    api.Created(newResource, "Resource created")
    return api
})

// Error
r.GET("/api/error", func() *response.ApiHelper {
    api := response.NewApiHelper()
    api.NotFound("Resource not found")
    return api
})

Standard Response Format:

Success:

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

Success with message:

{
  "status": "success",
  "message": "Operation successful",
  "data": { ... }
}

Error:

{
  "status": "error",
  "error": {
    "code": "NOT_FOUND",
    "message": "Resource not found"
  }
}

When to use:

Pros:

Cons:


πŸ”€ Understanding Response Paths

Path 1: Via Context (func(ctx *request.Context) error)

Write response using context helpers. Works for all response types.

// Manual - Direct control with ctx.W
r.GET("/manual/json", func(ctx *request.Context) error {
    ctx.W.Header().Set("Content-Type", "application/json")
    ctx.W.Write([]byte(`{"message":"hello"}`))
    return nil
})

// Generic Response - Using ctx.Resp (can return directly!)
r.GET("/ctx-resp/json", func(ctx *request.Context) error {
    return ctx.Resp.WithStatus(200).Json(data)
})

r.GET("/ctx-resp/html", func(ctx *request.Context) error {
    return ctx.Resp.Html("<html>...</html>")
})

// Opinionated API - Using ctx.Api (can return directly!)
r.GET("/ctx-api/success", func(ctx *request.Context) error {
    return ctx.Api.Ok(data)
})

r.GET("/ctx-api/error", func(ctx *request.Context) error {
    return ctx.Api.NotFound("Resource not found")
})

Characteristics:


Path 2: Via Return (func() T or func() (T, error))

Return response object or data. Works for all response types except manual.

// Return plain data (auto JSON)
r.GET("/return/data", func() any {
    return map[string]string{"message": "hello"}
})

// Return data with error
r.GET("/return/data-error", func() (any, error) {
    return data, nil
})

// Return Response object
r.GET("/return/response", func() *response.Response {
    resp := response.NewResponse()
    resp.Json(data)
    return resp
})

// Return Response with error handling
r.GET("/return/response-error", func() (*response.Response, error) {
    resp := response.NewResponse()
    resp.Json(data)
    return resp, nil
})

// Return ApiHelper object
r.GET("/return/api", func() *response.ApiHelper {
    api := response.NewApiHelper()
    api.Ok(data)
    return api
})

// Return ApiHelper with error handling
r.GET("/return/api-error", func() (*response.ApiHelper, error) {
    api := response.NewApiHelper()
    users, err := db.GetUsers()
    if err != nil {
        api.InternalError("Database error")
        return api, nil
    }
    api.Ok(users)
    return api, nil
})

Characteristics:


πŸ“Š Complete Response Matrix

Response Type Via Context Via Return Use Case
Manual βœ… ctx.W.Write() ❌ Not supported Streaming, binary
Generic Response βœ… ctx.Resp.Json() βœ… return resp or return resp, nil Mixed formats (JSON/HTML/text)
Opinionated API βœ… ctx.Api.Ok() βœ… return api or return api, nil REST APIs
Plain Data ❌ Not supported βœ… return data Simple JSON

Key Points:


πŸ§ͺ Test Examples

Manual Response

curl http://localhost:3000/manual/json
curl http://localhost:3000/manual/text

Response (manual/json):

{"message":"Manual JSON response","method":"http.ResponseWriter"}

Generic Response

curl http://localhost:3000/response/json
curl http://localhost:3000/response/html
curl http://localhost:3000/response/text
curl -i http://localhost:3000/response/custom-status

Response (response/json):

{
  "message": "Generic JSON using response.Response",
  "data": [...]
}

Opinionated API

curl http://localhost:3000/api/success
curl http://localhost:3000/api/success-message
curl -X POST http://localhost:3000/api/created
curl http://localhost:3000/api/error-notfound

Response (api/success):

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

Response (api/error-notfound):

{
  "status": "error",
  "error": {
    "code": "NOT_FOUND",
    "message": "User not found"
  }
}

Return Values

curl http://localhost:3000/return/data
curl http://localhost:3000/return/struct
curl http://localhost:3000/return/response
curl http://localhost:3000/return/api

Response (return/data):

{
  "message": "Direct data return",
  "users": [...],
  "count": 2
}

Comparison

# Same data, 4 different methods
curl http://localhost:3000/compare/manual
curl http://localhost:3000/compare/response
curl http://localhost:3000/compare/api
curl http://localhost:3000/compare/return

🎯 Decision Guide

Choose Response Type:

Need HTML/text/binary?
β”œβ”€ YES β†’ Use response.Response (generic)
β”‚
└─ NO (JSON only)
    β”œβ”€ Need consistent API structure?
    β”‚   β”œβ”€ YES β†’ Use response.ApiHelper ⭐ (recommended for APIs)
    β”‚   └─ NO β†’ Use response.Response or plain return
    β”‚
    └─ Need absolute control?
        └─ YES β†’ Use manual (http.ResponseWriter)

Choose Response Path:

Simple data return?
β”œβ”€ YES β†’ Use return path (func() T)
β”‚
└─ NO
    β”œβ”€ Need to read request headers/body?
    β”‚   └─ YES β†’ Use context path (func(ctx))
    β”‚
    └─ Need error handling?
        └─ YES β†’ Use return with error (func() (T, error))

πŸ’‘ Best Practices

1. For REST APIs: Use ApiHelper

// βœ… Recommended for APIs
r.GET("/users", func() (*response.ApiHelper, error) {
    api := response.NewApiHelper()
    users, err := db.GetUsers()
    if err != nil {
        api.InternalError("Database error")
        return api, nil
    }
    api.Ok(users)
    return api, nil
})

Why?


2. For Simple Data: Use Return

// βœ… Simplest for basic data
r.GET("/stats", func() any {
    return map[string]int{
        "users": 100,
        "posts": 500,
    }
})

3. For Mixed Content: Use response.Response

// βœ… When you need HTML, JSON, text, etc
r.GET("/page", func() (*response.Response, error) {
    resp := response.NewResponse()
    
    if acceptsJSON(req) {
        resp.Json(data)
    } else {
        resp.Html(htmlPage)
    }
    
    return resp, nil
})

4. For Streaming: Use Manual

// βœ… For SSE, file streaming, etc
r.GET("/stream", func(ctx *request.Context) error {
    ctx.W.Header().Set("Content-Type", "text/event-stream")
    
    for event := range events {
        fmt.Fprintf(ctx.W, "data: %s\n\n", event)
        ctx.W.(http.Flusher).Flush()
    }
    
    return nil
})

πŸ“‹ ApiHelper Methods Reference

Success Methods

api.Ok(data)                           // 200 OK
api.OkWithMessage(data, "message")     // 200 OK with message
api.Created(data, "message")           // 201 Created
api.OkList(data, meta)                 // 200 OK with pagination

Error Methods

api.BadRequest(code, message)          // 400 Bad Request
api.Unauthorized(message)              // 401 Unauthorized
api.Forbidden(message)                 // 403 Forbidden
api.NotFound(message)                  // 404 Not Found
api.InternalError(message)             // 500 Internal Server Error

πŸŽ“ What You Learned



Back: 04 - Handler Forms
Next: Ready to build a complete API!