Responses
Learn how to send responses from your Goose handlers.
Basic Responses
Controllers return values that are automatically serialized:
func (c *UserController) Show(ctx types.Context) any {
user := &User{ID: "1", Name: "John"}
return user // Automatically serialized
}
JSON Responses (API)
For API platforms, return values are serialized to JSON:
// Simple object
func (c *UserController) Show(ctx types.Context) any {
return &User{ID: "1", Name: "John"}
}
// Output: {"id": "1", "name": "John"}
// Array
func (c *UserController) List(ctx types.Context) any {
return []User{
{ID: "1", Name: "John"},
{ID: "2", Name: "Jane"},
}
}
// Output: [{"id": "1", "name": "John"}, {"id": "2", "name": "Jane"}]
// Map
func (c *UserController) Stats(ctx types.Context) any {
return map[string]any{
"total": 100,
"active": 85,
"inactive": 15,
}
}
// Output: {"total": 100, "active": 85, "inactive": 15}
HTML Responses (Web)
For web platforms, return data that your serializer processes:
func (c *WebController) Home(ctx types.Context) any {
return map[string]any{
"_template": "pages/home.html",
"title": "Welcome",
"user": currentUser,
}
}
Template Data
Pass data to templates:
func (c *WebController) UserProfile(ctx types.Context) any {
user := c.userService.GetByID(ctx.Request().Params()["id"])
posts := c.postService.GetByUser(user.ID)
return map[string]any{
"_template": "users/profile.html",
"user": user,
"posts": posts,
"stats": map[string]int{
"followers": 1000,
"following": 500,
},
}
}
Redirects
Return redirect information that your platform/serializer handles:
// Simple redirect
func (c *WebController) Logout(ctx types.Context) any {
c.authService.Logout(ctx)
return map[string]any{
"_redirect": "/",
}
}
// Redirect with status code (301 permanent)
func (c *WebController) OldPage(ctx types.Context) any {
return map[string]any{
"_redirect": "/new-page",
"_status": 301,
}
}
// Redirect after create
func (c *WebController) AfterCreate(ctx types.Context) any {
return map[string]any{
"_redirect": "/users/" + user.ID,
}
}
Error Responses
Return error responses as structured data:
// Simple error
func (c *UserController) Show(ctx types.Context) any {
user, err := c.service.GetByID(ctx.Request().Params()["id"])
if err != nil {
return map[string]any{
"error": "User not found",
"_status": 404,
}
}
return user
}
// Structured error
func (c *UserController) Create(ctx types.Context) any {
body, err := ctx.Request().Body()
if err != nil {
return map[string]any{
"error": "validation_failed",
"message": "Invalid request body",
"details": err.Error(),
"_status": 400,
}
}
var dto CreateUserDTO
if err := json.Unmarshal(body, &dto); err != nil {
return map[string]any{
"error": "validation_failed",
"message": "Invalid JSON",
"_status": 400,
}
}
return c.service.CreateUser(dto)
}
Status Codes
Setting Status Codes
Include status in your response for your serializer to handle:
func (c *UserController) Create(ctx types.Context) any {
user, err := c.service.CreateUser(dto)
if err != nil {
return map[string]any{
"error": "Failed to create user",
"_status": 500,
}
}
return map[string]any{
"data": user,
"_status": 201, // Created
}
}
Common Status Codes
| Code | Meaning | Usage |
|---|---|---|
| 200 | OK | Successful GET |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input |
| 401 | Unauthorized | Not authenticated |
| 403 | Forbidden | Not authorized |
| 404 | Not Found | Resource not found |
| 422 | Unprocessable Entity | Validation error |
| 500 | Internal Server Error | Server error |
Headers
Set response headers using the Response interface:
func (c *ApiController) Handle(ctx types.Context) any {
resp := ctx.Response()
// Single header
resp.SetHeader("X-Custom-Header", "value")
// Cache headers
resp.SetHeader("Cache-Control", "max-age=3600")
resp.SetHeader("ETag", "abc123")
// CORS headers
resp.SetHeader("Access-Control-Allow-Origin", "*")
// Multiple headers at once
resp.SetHeaders(map[string]string{
"X-Custom-1": "value1",
"X-Custom-2": "value2",
})
return data
}
Cookies
Set cookies using the raw response (platform-specific):
import "net/http"
func (c *AuthController) Login(ctx types.Context) any {
token := c.authService.CreateToken(user)
// For HTTP platforms, access raw response writer
raw := ctx.Response().Raw()
if w, ok := raw.(http.ResponseWriter); ok {
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: token,
MaxAge: 3600,
Path: "/",
Secure: true,
HttpOnly: true,
})
}
return map[string]string{"status": "logged in"}
}
File Downloads
Send files as downloads using raw response:
import (
"net/http"
"os"
)
func (c *FileController) Download(ctx types.Context) any {
fileID := ctx.Request().Params()["id"]
file := c.fileService.GetFile(fileID)
// Set download headers
resp := ctx.Response()
resp.SetHeader("Content-Disposition", "attachment; filename="+file.Name)
resp.SetHeader("Content-Type", "application/octet-stream")
// Read and return file content
content, err := os.ReadFile(file.Path)
if err != nil {
return map[string]string{"error": "File not found"}
}
return content
}
Streaming Responses
For large data, use raw response for streaming:
import (
"fmt"
"net/http"
)
func (c *ExportController) ExportCSV(ctx types.Context) any {
resp := ctx.Response()
resp.SetHeader("Content-Type", "text/csv")
resp.SetHeader("Content-Disposition", "attachment; filename=export.csv")
// For HTTP platforms, stream directly to response writer
raw := resp.Raw()
if w, ok := raw.(http.ResponseWriter); ok {
users := c.userService.GetAll()
for _, user := range users {
fmt.Fprintf(w, "%s,%s\n", user.ID, user.Name)
}
}
return nil
}
Response Helpers
Success Response Pattern
func Success(data any) map[string]any {
return map[string]any{
"success": true,
"data": data,
}
}
func (c *UserController) List(ctx types.Context) any {
users := c.service.GetAll()
return Success(users)
}
// Output: {"success": true, "data": [...]}
Pagination Response
func Paginated(data any, page, limit, total int) map[string]any {
return map[string]any{
"data": data,
"meta": map[string]int{
"page": page,
"limit": limit,
"total": total,
"totalPages": (total + limit - 1) / limit,
},
}
}
func (c *UserController) List(ctx types.Context) any {
queries := ctx.Request().Queries()
page, _ := strconv.Atoi(queries["page"])
if page == 0 {
page = 1
}
limit, _ := strconv.Atoi(queries["limit"])
if limit == 0 {
limit = 10
}
users, total := c.service.Paginate(page, limit)
return Paginated(users, page, limit, total)
}
CLI Output
For CLI applications:
func (c *CliController) ListUsers(ctx types.Context) any {
users := c.service.GetAll()
// Simple string output
return fmt.Sprintf("Found %d users", len(users))
}
// Formatted table
func (c *CliController) ListUsers(ctx types.Context) any {
users := c.service.GetAll()
var output strings.Builder
output.WriteString("ID\tName\tEmail\n")
output.WriteString("--\t----\t-----\n")
for _, user := range users {
output.WriteString(fmt.Sprintf("%s\t%s\t%s\n",
user.ID, user.Name, user.Email))
}
return output.String()
}
Best Practices
1. Consistent Response Structure
// Define standard response format
type APIResponse struct {
Success bool `json:"success"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
func (c *UserController) Show(ctx types.Context) any {
user, err := c.service.GetByID(ctx.Request().Params()["id"])
if err != nil {
return APIResponse{Success: false, Error: err.Error()}
}
return APIResponse{Success: true, Data: user}
}
2. Use Appropriate Status Codes
func (c *UserController) Create(ctx types.Context) any {
user, err := c.service.Create(dto)
if err != nil {
if errors.Is(err, ErrDuplicate) {
return map[string]any{
"error": "User already exists",
"_status": 409, // Conflict
}
}
return map[string]any{
"error": "Failed to create user",
"_status": 500,
}
}
return map[string]any{
"data": user,
"_status": 201, // Created
}
}
3. Don't Leak Sensitive Data
// โ
Good: Use DTOs for responses
type UserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
// Password omitted
}
// โ Bad: Returning entity directly
func (c *UserController) Show(ctx types.Context) any {
return c.service.GetByID(id) // May include password hash
}
Next Steps
- Requests - Handling requests
- Error Handling - Error responses
- Validation - Input validation