Response
Response building and formatting helpers
Overview
Lokstra provides two response helpers:
Response- Low-level response builder (flexible)ApiHelper- High-level API response formatter (opinionated, recommended)
Both are available through the request context (c.Resp and c.Api) or via helper constructors.
Import Path
import "github.com/primadi/lokstra/core/response"
// Method 1: Helper constructors (quick one-liner)
func handler(params *Params) *response.Response {
return response.NewJsonResponse(data)
}
func apiHandler(params *Params) *response.ApiHelper {
return response.NewApiOk(data)
}
// Method 2: Context access (chainable methods)
func handler(c *lokstra.RequestContext) error {
return c.Resp.WithStatus(200).Json(data) // Low-level
}
func apiHandler(c *lokstra.RequestContext) error {
return c.Api.Ok(data) // High-level
}
ApiHelper (Recommended)
High-level helper that formats responses according to a standard API structure.
Usage Methods
Method 1: Helper Constructors (Quick One-Liner)
func getUser(params *getUserParams) *response.ApiHelper {
user := getUserFromDB(params.id)
return response.NewApiOk(user) // Returns *ApiHelper directly
}
Method 2: Context Methods (Chainable)
func getUser(c *lokstra.RequestContext) error {
user := getUserFromDB(c.Req.Param("id"))
return c.Api.Ok(user) // Returns error
}
Method 3: Manual Creation
func getUser() *response.ApiHelper {
user := getUserFromDB(id)
api := response.NewApiHelper()
api.Ok(user)
return api
}
Recommendation: Use Method 1 (helper constructors) for clean one-liner returns, or Method 2 (context methods) when you need request context.
Success Responses
Ok
Sends successful response with data.
Signatures:
// Constructor (returns *ApiHelper)
func NewApiOk(data any) *ApiHelper
// Context method (returns error)
func (a *ApiHelper) Ok(data any) error
Parameters:
data- Response data (any JSON-serializable type)
Examples:
// Using constructor
func getUser(params *getUserParams) *response.ApiHelper {
user := getUserFromDB(params.id)
return response.NewApiOk(user)
}
// Using context
func getUser(c *lokstra.RequestContext) error {
user := getUserFromDB(c.Req.Param("id"))
return c.Api.Ok(user)
}
// Response (HTTP 200):
// {
// "status": "success",
// "data": { "id": 1, "name": "John" }
// }
OkWithMessage
Sends successful response with message and data.
Signatures:
// Constructor
func NewApiOkWithMessage(data any, message string) *ApiHelper
// Context method
func (a *ApiHelper) OkWithMessage(data any, message string) error
Examples:
// Using constructor
func updateUser(params *updateUserParams) *response.ApiHelper {
user := updateUserInDB(params.id, params.input)
return response.NewApiOkWithMessage(user, "User updated successfully")
}
// Using context
func updateUser(c *lokstra.RequestContext) error {
user := updateUserInDB(id, input)
return c.Api.OkWithMessage(user, "User updated successfully")
}
// Response (HTTP 200):
// {
// "status": "success",
// "message": "User updated successfully",
// "data": { "id": 1, "name": "John" }
// }
Created
Sends 201 Created response.
Signatures:
// Constructor
func NewApiCreated(data any, message string) *ApiHelper
// Context method
func (a *ApiHelper) Created(data any, message string) error
Examples:
// Using constructor
func createUser(input *CreateUserInput) *response.ApiHelper {
user := createUserInDB(input)
return response.NewApiCreated(user, "User created successfully")
}
// Using context
func createUser(c *lokstra.RequestContext, input *CreateUserInput) error {
user := createUserInDB(input)
return c.Api.Created(user, "User created successfully")
}
// Response (HTTP 201):
// {
// "status": "success",
// "message": "User created successfully",
// "data": { "id": 1, "name": "John" }
// }
List Responses
OkList
Sends paginated list response.
Signatures:
// Constructor
func NewApiOkList(data any, meta *api_formatter.ListMeta) *ApiHelper
// Context method
func (a *ApiHelper) OkList(data any, meta *api_formatter.ListMeta) error
Parameters:
data- List data (slice)meta- Pagination metadata
Examples:
// Using constructor
func listUsers(params *listUsersParams) *response.ApiHelper {
users := getUsersFromDB(params.page, params.limit)
total := countUsers()
meta := &api_formatter.ListMeta{
Page: params.page,
Limit: params.limit,
Total: total,
TotalPages: (total + params.limit - 1) / params.limit,
}
return response.NewApiOkList(users, meta)
}
// Using context
func listUsers(c *lokstra.RequestContext) error {
users := getUsersFromDB(page, limit)
total := countUsers()
meta := &api_formatter.ListMeta{
Page: page,
Limit: limit,
Total: total,
TotalPages: (total + limit - 1) / limit,
}
return c.Api.OkList(users, meta)
}
// Response (HTTP 200):
// {
// "status": "success",
// "data": [
// { "id": 1, "name": "John" },
// { "id": 2, "name": "Jane" }
// ],
// "meta": {
// "page": 1,
// "limit": 20,
// "total": 42,
// "total_pages": 3
// }
// }
Error Responses
BadRequest
Sends 400 Bad Request error.
Signatures:
// Constructor
func NewApiBadRequest(code, message string) *ApiHelper
// Context method
func (a *ApiHelper) BadRequest(code, message string) error
Examples:
// Using constructor
func validateInput(input *Input) *response.ApiHelper {
if input.Amount <= 0 {
return response.NewApiBadRequest("INVALID_AMOUNT", "Amount must be positive")
}
// ...
}
// Using context
func handler(c *lokstra.RequestContext) error {
if input.Amount <= 0 {
return c.Api.BadRequest("INVALID_AMOUNT", "Amount must be positive")
}
// ...
}
// Response (HTTP 400):
// {
// "status": "error",
// "error": {
// "code": "INVALID_AMOUNT",
// "message": "Amount must be positive"
// }
// }
Unauthorized
Sends 401 Unauthorized error.
Signatures:
// Constructor
func NewApiUnauthorized(message string) *ApiHelper
// Context method
func (a *ApiHelper) Unauthorized(message string) error
Examples:
// Using constructor
func checkAuth(token string) *response.ApiHelper {
if token == "" {
return response.NewApiUnauthorized("Missing authorization token")
}
// ...
}
// Using context
func authMiddleware(c *lokstra.RequestContext) error {
token := c.Req.Header("Authorization")
if token == "" {
return c.Api.Unauthorized("Missing authorization token")
}
return c.Next()
}
// Response (HTTP 401):
// {
// "status": "error",
// "error": {
// "code": "UNAUTHORIZED",
// "message": "Missing authorization token"
// }
// }
Forbidden
Sends 403 Forbidden error.
Signatures:
// Constructor
func NewApiForbidden(message string) *ApiHelper
// Context method
func (a *ApiHelper) Forbidden(message string) error
Examples:
// Using constructor
func checkPermission(user *User) *response.ApiHelper {
if !user.IsAdmin {
return response.NewApiForbidden("Admin access required")
}
// ...
}
// Using context
func deleteUser(c *lokstra.RequestContext) error {
user := c.Get("user").(*User)
if !user.IsAdmin {
return c.Api.Forbidden("Admin access required")
}
// ...
}
// Response (HTTP 403):
// {
// "status": "error",
// "error": {
// "code": "FORBIDDEN",
// "message": "Admin access required"
// }
// }
NotFound
Sends 404 Not Found error.
Signatures:
// Constructor
func NewApiNotFound(message string) *ApiHelper
// Context method
func (a *ApiHelper) NotFound(message string) error
Examples:
// Using constructor
func getUser(params *getUserParams) *response.ApiHelper {
user, err := getUserFromDB(params.id)
if err != nil {
return response.NewApiNotFound("User not found")
}
return response.NewApiOk(user)
}
// Using context
func getUser(c *lokstra.RequestContext) error {
id := c.Req.Param("id")
user, err := getUserFromDB(id)
if err != nil {
return c.Api.NotFound("User not found")
}
return c.Api.Ok(user)
}
// Response (HTTP 404):
// {
// "status": "error",
// "error": {
// "code": "NOT_FOUND",
// "message": "User not found"
// }
// }
InternalError
Sends 500 Internal Server Error.
Signatures:
// Constructor
func NewApiInternalError(message string) *ApiHelper
// Context method
func (a *ApiHelper) InternalError(message string) error
Examples:
// Using constructor
func processData(data *Data) *response.ApiHelper {
if err := processingLogic(data); err != nil {
log.Printf("Processing error: %v", err)
return response.NewApiInternalError("Failed to process data")
}
return response.NewApiOk(result)
}
// Using context
func handler(c *lokstra.RequestContext) error {
user, err := getUserFromDB(id)
if err != nil {
log.Printf("Database error: %v", err)
return c.Api.InternalError("Failed to fetch user")
}
return c.Api.Ok(user)
}
// Response (HTTP 500):
// {
// "status": "error",
// "error": {
// "code": "INTERNAL_ERROR",
// "message": "Failed to fetch user"
// }
// }
ValidationError
Sends 400 validation error with field details.
Signatures:
// Constructor
func NewApiValidationError(message string, fields []api_formatter.FieldError) *ApiHelper
// Context method
func (a *ApiHelper) ValidationError(message string, fields []api_formatter.FieldError) error
Examples:
// Using constructor
func validateUser(input *CreateUserInput) *response.ApiHelper {
var fieldErrors []api_formatter.FieldError
if !isValidEmail(input.Email) {
fieldErrors = append(fieldErrors, api_formatter.FieldError{
Field: "email",
Code: "INVALID_FORMAT",
Message: "Email format is invalid",
})
}
if len(fieldErrors) > 0 {
return response.NewApiValidationError("Validation failed", fieldErrors)
}
// ...
}
// Using context (automatic via BindJSON)
func createUser(c *lokstra.RequestContext) error {
var input CreateUserInput
if err := c.Req.BindJSON(&input); err != nil {
// BindJSON automatically returns ValidationError
return err
}
// ...
}
// Response (HTTP 400):
// {
// "status": "error",
// "error": {
// "code": "VALIDATION_ERROR",
// "message": "Validation failed",
// "fields": [
// {
// "field": "email",
// "code": "INVALID_FORMAT",
// "message": "Email format is invalid"
// },
// {
// "field": "age",
// "code": "MIN_VALUE",
// "message": "Age must be at least 18"
// }
// ]
// }
// }
Error (Generic)
Sends error response with custom status code and error code.
Signatures:
// Constructor
func NewApiError(statusCode int, code, message string) *ApiHelper
// Context method
func (a *ApiHelper) Error(statusCode int, code, message string) error
Examples:
// Using constructor
func checkQuota() *response.ApiHelper {
if quota.Exceeded() {
return response.NewApiError(429, "QUOTA_EXCEEDED", "API quota exceeded")
}
// ...
}
// Using context
func handler(c *lokstra.RequestContext) error {
if quota.Exceeded() {
return c.Api.Error(429, "QUOTA_EXCEEDED", "API quota exceeded")
}
// ...
}
// Response (HTTP 429):
// {
// "status": "error",
// "error": {
// "code": "QUOTA_EXCEEDED",
// "message": "API quota exceeded"
// }
// }
Response (Low-Level)
Low-level response builder for custom response formats.
Helper Constructors
Quick one-liner constructors for common response types:
// JSON response
func NewJsonResponse(data any) *Response
// HTML response
func NewHtmlResponse(html string) *Response
// Plain text response
func NewTextResponse(text string) *Response
// Custom content-type (CSV, XML, PDF, etc.)
func NewRawResponse(contentType string, b []byte) *Response
// Streaming response (SSE, chunked transfer)
func NewStreamResponse(contentType string, fn func(w http.ResponseWriter) error) *Response
Examples:
// JSON
func getUsers() *response.Response {
users := getUsersFromDB()
return response.NewJsonResponse(users)
}
// HTML
func homepage() *response.Response {
html := "<html><body><h1>Welcome</h1></body></html>"
return response.NewHtmlResponse(html)
}
// Text
func healthCheck() *response.Response {
return response.NewTextResponse("OK")
}
// CSV
func exportData() *response.Response {
csvData := generateCSV()
return response.NewRawResponse("text/csv", csvData)
}
// Stream
func streamFile() *response.Response {
return response.NewStreamResponse("application/octet-stream", func(w http.ResponseWriter) error {
return streamFileContent(w)
})
}
Chainable Methods
WithStatus
Sets HTTP status code.
Signature:
func (r *Response) WithStatus(code int) *Response
Example:
// Chainable method
func handler(c *lokstra.RequestContext) error {
return c.Resp.WithStatus(200).Json(data)
}
// Or manual creation
func handler() *response.Response {
r := response.NewResponse()
return r.WithStatus(201).Json(data)
}
Status Code Constants:
http.StatusOK // 200
http.StatusCreated // 201
http.StatusAccepted // 202
http.StatusNoContent // 204
http.StatusBadRequest // 400
http.StatusUnauthorized // 401
http.StatusForbidden // 403
http.StatusNotFound // 404
http.StatusInternalServerError // 500
// ... see net/http package for full list
Json
Sends JSON response.
Signature:
func (r *Response) Json(data any) error
Examples:
// Using helper constructor (recommended)
func handler(params *Params) *response.Response {
data := map[string]any{
"message": "Hello",
"users": users,
}
return response.NewJsonResponse(data)
}
// Using context (chainable)
func handler(c *lokstra.RequestContext) error {
return c.Resp.WithStatus(200).Json(data)
}
Html
Sends HTML response.
Signature:
func (r *Response) Html(html string) error
Examples:
// Using helper constructor (recommended)
func homepage() *response.Response {
html := `<html><body><h1>Welcome</h1></body></html>`
return response.NewHtmlResponse(html)
}
// Using context (chainable)
func homepage(c *lokstra.RequestContext) error {
html := `<html><body><h1>Welcome</h1></body></html>`
return c.Resp.WithStatus(200).Html(html)
}
Text
Sends plain text response.
Signature:
func (r *Response) Text(text string) error
Examples:
// Using helper constructor (recommended)
func healthCheck() *response.Response {
return response.NewTextResponse("OK")
}
// Using context (chainable)
func healthCheck(c *lokstra.RequestContext) error {
return c.Resp.WithStatus(200).Text("OK")
}
Raw
Sends raw bytes with custom content type.
Signature:
func (r *Response) Raw(contentType string, b []byte) error
Examples:
// Using helper constructor (recommended)
func downloadFile(filename string) *response.Response {
data := readFileBytes(filename)
return response.NewRawResponse("application/pdf", data)
}
// Using context (chainable)
func downloadFile(c *lokstra.RequestContext) error {
data := readFileBytes(filename)
return c.Resp.WithStatus(200).Raw("application/pdf", data)
}
Stream
Streams response using custom writer function.
Signature:
func (r *Response) Stream(contentType string, fn func(w http.ResponseWriter) error) error
Examples:
// Using helper constructor (recommended)
func streamLargeFile(filename string) *response.Response {
return response.NewStreamResponse("application/octet-stream", func(w http.ResponseWriter) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(w, file)
return err
})
}
// Using context (chainable)
func streamLargeFile(c *lokstra.RequestContext) error {
return c.Resp.WithStatus(200).Stream("application/octet-stream", func(w http.ResponseWriter) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(w, file)
return err
})
}
Complete Examples
CRUD API with ApiHelper
Using Helper Constructors (Recommended for handlers without context)
func listUsers(params *listUsersParams) *response.ApiHelper {
users := getUsersFromDB(params.page, params.limit)
total := countUsers()
meta := &api_formatter.ListMeta{
Page: params.page,
Limit: params.limit,
Total: total,
TotalPages: (total + params.limit - 1) / params.limit,
}
return response.NewApiOkList(users, meta)
}
func getUser(params *getUserParams) *response.ApiHelper {
user, err := getUserFromDB(params.id)
if err != nil {
return response.NewApiNotFound("User not found")
}
return response.NewApiOk(user)
}
func createUser(input *CreateUserInput) *response.ApiHelper {
user, err := createUserInDB(input)
if err != nil {
return response.NewApiInternalError("Failed to create user")
}
return response.NewApiCreated(user, "User created successfully")
}
func updateUser(params *updateUserParams, input *UpdateUserInput) *response.ApiHelper {
user, err := updateUserInDB(params.id, input)
if err != nil {
return response.NewApiInternalError("Failed to update user")
}
return response.NewApiOkWithMessage(user, "User updated successfully")
}
func deleteUser(params *deleteUserParams) *response.ApiHelper {
if err := deleteUserFromDB(params.id); err != nil {
return response.NewApiInternalError("Failed to delete user")
}
// Note: NoContent doesn't have a constructor, use context method or manual
api := response.NewApiHelper()
api.resp.WithStatus(http.StatusNoContent)
return api
}
Using Context Methods (When you need request context)
func listUsers(c *lokstra.RequestContext) error {
page := getIntParam(c.Req.Query("page", "1"))
limit := getIntParam(c.Req.Query("limit", "20"))
users := getUsersFromDB(page, limit)
total := countUsers()
meta := &api_formatter.ListMeta{
Page: page,
Limit: limit,
Total: total,
TotalPages: (total + limit - 1) / limit,
}
return c.Api.OkList(users, meta)
}
func getUser(c *lokstra.RequestContext) error {
id := c.Req.Param("id")
user, err := getUserFromDB(id)
if err != nil {
return c.Api.NotFound("User not found")
}
return c.Api.Ok(user)
}
func createUser(c *lokstra.RequestContext, input *CreateUserInput) error {
user, err := createUserInDB(input)
if err != nil {
return c.Api.InternalError("Failed to create user")
}
return c.Api.Created(user, "User created successfully")
}
func updateUser(c *lokstra.RequestContext, input *UpdateUserInput) error {
id := c.Req.Param("id")
user, err := updateUserInDB(id, input)
if err != nil {
return c.Api.InternalError("Failed to update user")
}
return c.Api.OkWithMessage(user, "User updated successfully")
}
Custom Response Format (Low-Level)
Using Helper Constructors (Recommended)
func customResponse() *response.Response {
// Custom JSON structure
data := map[string]any{
"version": "1.0",
"timestamp": time.Now().Unix(),
"data": map[string]any{
"users": users,
"count": len(users),
},
}
return response.NewJsonResponse(data)
}
func xmlResponse() *response.Response {
xml := `<?xml version="1.0"?>
<users>
<user id="1">John</user>
<user id="2">Jane</user>
</users>`
return response.NewRawResponse("application/xml", []byte(xml))
}
func csvExport() *response.Response {
csv := generateCSVBytes()
return response.NewRawResponse("text/csv", csv)
}
Using Context Methods
func customResponse(c *lokstra.RequestContext) error {
data := map[string]any{
"version": "1.0",
"timestamp": time.Now().Unix(),
"data": map[string]any{
"users": users,
"count": len(users),
},
}
return c.Resp.WithStatus(200).Json(data)
}
func xmlResponse(c *lokstra.RequestContext) error {
xml := `<?xml version="1.0"?>
<users>
<user id="1">John</user>
<user id="2">Jane</user>
</users>`
return c.Resp.WithStatus(200).Raw("application/xml", []byte(xml))
}
File Download
Using Helper Constructor
func downloadReport(reportID string) *response.Response {
// Generate or fetch report
data := generateReport(reportID)
// Create response with helper
r := response.NewRawResponse("application/pdf", data)
// Set headers for download
r.RespHeaders = map[string][]string{
"Content-Disposition": {fmt.Sprintf("attachment; filename=report-%s.pdf", reportID)},
}
return r
}
Using Context Method
func downloadReport(c *lokstra.RequestContext) error {
reportID := c.Req.Param("id")
// Generate or fetch report
data := generateReport(reportID)
// Set headers for download
c.Resp.RespHeaders = map[string][]string{
"Content-Disposition": {fmt.Sprintf("attachment; filename=report-%s.pdf", reportID)},
}
return c.Resp.WithStatus(200).Raw("application/pdf", data)
}
Streaming Response
Using Helper Constructor
func streamEvents() *response.Response {
return response.NewStreamResponse("text/event-stream", func(w http.ResponseWriter) error {
flusher, ok := w.(http.Flusher)
if !ok {
return fmt.Errorf("streaming not supported")
}
for i := 0; i < 10; i++ {
fmt.Fprintf(w, "data: Event %d\n\n", i)
flusher.Flush()
time.Sleep(1 * time.Second)
}
return nil
})
}
Using Context Method
func streamEvents(c *lokstra.RequestContext) error {
return c.Resp.WithStatus(200).Stream("text/event-stream", func(w http.ResponseWriter) error {
flusher, ok := w.(http.Flusher)
if !ok {
return fmt.Errorf("streaming not supported")
}
for i := 0; i < 10; i++ {
fmt.Fprintf(w, "data: Event %d\n\n", i)
flusher.Flush()
time.Sleep(1 * time.Second)
}
return nil
})
}
Response Formatters
Custom Response Format
You can customize the API response format:
import "github.com/primadi/lokstra/core/response"
// Set custom formatter globally
response.SetApiResponseFormatter(myCustomFormatter)
// Or use built-in formatters by name
response.SetApiResponseFormatterByName("default")
response.SetApiResponseFormatterByName("custom-name")
Best Practices
1. Use Helper Constructors for Clean Code
// β
Recommended: Helper constructors (one-liner)
func getUser(params *getUserParams) *response.ApiHelper {
return response.NewApiOk(user)
}
func homepage() *response.Response {
return response.NewHtmlResponse(html)
}
// β
Good: Context methods when you need request context
func getUser(c *lokstra.RequestContext) error {
return c.Api.Ok(user)
}
// π« Avoid: Manual creation when helpers exist
func getUser() *response.ApiHelper {
api := response.NewApiHelper()
api.Ok(user)
return api
}
2. Use ApiHelper for REST APIs
// β
Recommended: ApiHelper for structured API responses
return response.NewApiOk(user)
return c.Api.Ok(user)
// π« Avoid: Manual JSON structure for standard APIs
return response.NewJsonResponse(map[string]any{"data": user})
3. Choose the Right Response Type
// β
Good: Use Response for custom formats
func homepage() *response.Response {
return response.NewHtmlResponse(html)
}
func exportCSV() *response.Response {
return response.NewRawResponse("text/csv", csvData)
}
// β
Good: Use ApiHelper for REST APIs
func getUser(params *getUserParams) *response.ApiHelper {
return response.NewApiOk(user)
}
4. Consistent Error Codes
// β
Good: Use consistent error codes
const (
ErrInvalidInput = "INVALID_INPUT"
ErrUserNotFound = "USER_NOT_FOUND"
ErrUnauthorized = "UNAUTHORIZED"
)
return response.NewApiError(400, ErrInvalidInput, "Invalid user data")
// π« Avoid: Random error messages
return response.NewApiBadRequest("error123", "something went wrong")
5. Donβt Log Sensitive Data in Responses
// β
Good
log.Printf("Failed to authenticate user: %v", err)
return response.NewApiUnauthorized("Authentication failed")
// π« Avoid
return response.NewApiUnauthorized(fmt.Sprintf("Auth failed: %v", err))
See Also
- Request Context - Request handling
- Router - Handler registration
- API Formatter - Custom formatters
Related Guides
- Router Essentials - Handler basics
- API Design - Best practices