Examples
Real-world examples showing how to build production-ready APIs with Steel.
E-commerce API
A complete e-commerce API with products, orders, and user management:
package main
import (
"fmt"
"log"
"net/http"
"time"
router "github.com/xraph/steel"
)
// Domain Models
type User struct {
ID int `json:"id" description:"User ID"`
Email string `json:"email" description:"User email"`
Name string `json:"name" description:"User full name"`
Role string `json:"role" description:"User role (customer, admin)"`
Created time.Time `json:"created" description:"Account creation date"`
}
type Product struct {
ID int `json:"id" description:"Product ID"`
Name string `json:"name" description:"Product name"`
Description string `json:"description" description:"Product description"`
Price float64 `json:"price" description:"Product price in USD"`
Stock int `json:"stock" description:"Available stock quantity"`
Category string `json:"category" description:"Product category"`
ImageURL string `json:"image_url" description:"Product image URL"`
}
type Order struct {
ID int `json:"id" description:"Order ID"`
UserID int `json:"user_id" description:"Customer user ID"`
Items []OrderItem `json:"items" description:"Order items"`
Total float64 `json:"total" description:"Order total amount"`
Status string `json:"status" description:"Order status"`
Created time.Time `json:"created" description:"Order creation date"`
}
type OrderItem struct {
ProductID int `json:"product_id" description:"Product ID"`
Quantity int `json:"quantity" description:"Item quantity"`
Price float64 `json:"price" description:"Item price at time of order"`
}
// Request/Response Types
type GetProductsRequest struct {
Category string `query:"category" description:"Filter by category"`
MinPrice float64 `query:"min_price" description:"Minimum price filter"`
MaxPrice float64 `query:"max_price" description:"Maximum price filter"`
Page int `query:"page" description:"Page number (default: 1)"`
Limit int `query:"limit" description:"Items per page (default: 20, max: 100)"`
Search string `query:"search" description:"Search in product names"`
}
type ProductListResponse struct {
Products []Product `json:"products" description:"List of products"`
Total int `json:"total" description:"Total number of products"`
Page int `json:"page" description:"Current page"`
HasMore bool `json:"has_more" description:"Whether there are more pages"`
}
type CreateOrderRequest struct {
Items []struct {
ProductID int `json:"product_id" description:"Product ID"`
Quantity int `json:"quantity" description:"Desired quantity"`
} `json:"items" body:"body" description:"Order items"`
ShippingAddress struct {
Street string `json:"street" description:"Street address"`
City string `json:"city" description:"City"`
State string `json:"state" description:"State/Province"`
ZipCode string `json:"zip_code" description:"Postal code"`
Country string `json:"country" description:"Country"`
} `json:"shipping_address" body:"body" description:"Shipping address"`
}
func main() {
r := router.NewRouter()
// Middleware
r.Use(router.Logger)
r.Use(router.Recoverer)
r.Use(corsMiddleware())
// API v1
r.Route("/api/v1", func(api router.Router) {
// Public endpoints
api.OpinionatedGET("/products", getProducts,
router.WithSummary("List Products"),
router.WithDescription("Get paginated list of products with filtering"),
router.WithTags("products"))
api.OpinionatedGET("/products/:id", getProduct,
router.WithSummary("Get Product"),
router.WithDescription("Get detailed product information"),
router.WithTags("products"))
// Authentication required
api.Route("/auth", func(auth router.Router) {
auth.Use(authMiddleware())
// User management
auth.OpinionatedGET("/profile", getUserProfile,
router.WithSummary("Get User Profile"),
router.WithTags("users"))
auth.OpinionatedPUT("/profile", updateUserProfile,
router.WithSummary("Update User Profile"),
router.WithTags("users"))
// Order management
auth.OpinionatedPOST("/orders", createOrder,
router.WithSummary("Create Order"),
router.WithDescription("Create a new order with items and shipping"),
router.WithTags("orders"))
auth.OpinionatedGET("/orders", getUserOrders,
router.WithSummary("Get User Orders"),
router.WithTags("orders"))
auth.OpinionatedGET("/orders/:id", getOrder,
router.WithSummary("Get Order"),
router.WithTags("orders"))
})
// Admin endpoints
api.Route("/admin", func(admin router.Router) {
admin.Use(authMiddleware())
admin.Use(adminMiddleware())
admin.OpinionatedPOST("/products", createProduct,
router.WithSummary("Create Product"),
router.WithTags("products", "admin"))
admin.OpinionatedPUT("/products/:id", updateProduct,
router.WithSummary("Update Product"),
router.WithTags("products", "admin"))
admin.OpinionatedGET("/orders", getAllOrders,
router.WithSummary("Get All Orders"),
router.WithTags("orders", "admin"))
})
})
// Health check
r.GET("/health", func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"healthy","timestamp":"` + time.Now().Format(time.RFC3339) + `"}`))
})
// Enable documentation
r.EnableOpenAPI()
log.Println("🛍️ E-commerce API starting on :8080")
log.Println("📚 API docs at http://localhost:8080/openapi/docs")
log.Fatal(http.ListenAndServe(":8080", r))
}
// Handlers
func getProducts(ctx *router.Context, req GetProductsRequest) (*ProductListResponse, error) {
// Set defaults
if req.Page <= 0 {
req.Page = 1
}
if req.Limit <= 0 {
req.Limit = 20
}
if req.Limit > 100 {
req.Limit = 100
}
// Mock data - replace with actual database queries
products := []Product{
{ID: 1, Name: "MacBook Pro", Price: 1999.99, Stock: 10, Category: "electronics"},
{ID: 2, Name: "iPhone 15", Price: 999.99, Stock: 25, Category: "electronics"},
{ID: 3, Name: "Nike Air Max", Price: 129.99, Stock: 50, Category: "shoes"},
}
// Apply filters (simplified)
filtered := filterProducts(products, req)
return &ProductListResponse{
Products: filtered,
Total: len(filtered),
Page: req.Page,
HasMore: false, // Calculate based on pagination
}, nil
}
func createOrder(ctx *router.Context, req CreateOrderRequest) (*Order, error) {
userID := getUserIDFromContext(ctx)
// Validate items
if len(req.Items) == 0 {
return nil, router.BadRequest("Order must contain at least one item")
}
var total float64
var orderItems []OrderItem
for _, item := range req.Items {
// Check product exists and has sufficient stock
product, err := getProductByID(item.ProductID)
if err != nil {
return nil, router.BadRequest(fmt.Sprintf("Product %d not found", item.ProductID))
}
if product.Stock < item.Quantity {
return nil, router.Conflict(fmt.Sprintf("Insufficient stock for product %d", item.ProductID))
}
orderItem := OrderItem{
ProductID: item.ProductID,
Quantity: item.Quantity,
Price: product.Price,
}
orderItems = append(orderItems, orderItem)
total += product.Price * float64(item.Quantity)
}
// Create order
order := &Order{
ID: generateOrderID(),
UserID: userID,
Items: orderItems,
Total: total,
Status: "pending",
Created: time.Now(),
}
// Save to database and update inventory
saveOrder(order)
updateInventory(req.Items)
return order, nil
}
// Middleware
func corsMiddleware() router.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
}
func authMiddleware() router.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
// Validate token (simplified)
userID, err := validateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Add user to context
ctx := context.WithValue(r.Context(), "user_id", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}Real-time Chat Application
WebSocket-based chat with rooms and user presence:
package main
import (
"fmt"
"log"
"net/http"
"sync"
"time"
router "github.com/xraph/steel"
)
// Chat Models
type ChatRoom struct {
ID string `json:"id"`
Name string `json:"name"`
Users map[string]*User `json:"users"`
Messages []ChatMessage `json:"messages"`
mu sync.RWMutex
}
type ChatMessage struct {
ID string `json:"id" description:"Message ID"`
UserID string `json:"user_id" description:"User who sent the message"`
Username string `json:"username" description:"Username"`
RoomID string `json:"room_id" description:"Room ID"`
Content string `json:"content" description:"Message content"`
Type string `json:"type" description:"Message type (text, system, image)"`
Timestamp time.Time `json:"timestamp" description:"Message timestamp"`
}
type ChatUser struct {
ID string `json:"id" description:"User ID"`
Username string `json:"username" description:"Username"`
Avatar string `json:"avatar" description:"Avatar URL"`
Status string `json:"status" description:"User status (online, away, offline)"`
JoinedAt time.Time `json:"joined_at" description:"When user joined the room"`
}
// WebSocket Message Types
type IncomingMessage struct {
Type string `json:"type" description:"Message type"`
RoomID string `json:"room_id" description:"Target room ID"`
Content string `json:"content" description:"Message content"`
Data map[string]interface{} `json:"data,omitempty" description:"Additional data"`
}
type OutgoingMessage struct {
Type string `json:"type" description:"Message type"`
Message ChatMessage `json:"message,omitempty" description:"Chat message"`
User ChatUser `json:"user,omitempty" description:"User information"`
Users []ChatUser `json:"users,omitempty" description:"List of users"`
RoomID string `json:"room_id" description:"Room ID"`
Timestamp time.Time `json:"timestamp" description:"Message timestamp"`
Error string `json:"error,omitempty" description:"Error message"`
}
// Room Manager
type RoomManager struct {
rooms map[string]*ChatRoom
mu sync.RWMutex
}
func NewRoomManager() *RoomManager {
return &RoomManager{
rooms: make(map[string]*ChatRoom),
}
}
func (rm *RoomManager) GetRoom(roomID string) *ChatRoom {
rm.mu.RLock()
defer rm.mu.RUnlock()
return rm.rooms[roomID]
}
func (rm *RoomManager) CreateRoom(roomID, name string) *ChatRoom {
rm.mu.Lock()
defer rm.mu.Unlock()
room := &ChatRoom{
ID: roomID,
Name: name,
Users: make(map[string]*User),
Messages: make([]ChatMessage, 0),
}
rm.rooms[roomID] = room
return room
}
var roomManager = NewRoomManager()
func main() {
r := router.NewRouter()
// Middleware
r.Use(router.Logger)
r.Use(router.Recoverer)
r.Use(corsMiddleware())
// WebSocket chat endpoint
r.WebSocket("/ws/chat/:room_id", handleChatConnection,
router.WithAsyncSummary("Chat WebSocket"),
router.WithAsyncDescription("Real-time chat communication in rooms"),
router.WithAsyncTags("chat", "websocket"))
// SSE for presence updates
r.SSE("/sse/presence/:room_id", handlePresenceStream,
router.WithAsyncSummary("Presence Updates"),
router.WithAsyncDescription("Real-time user presence updates"),
router.WithAsyncTags("presence", "sse"))
// REST API for chat history
r.Route("/api/v1", func(api router.Router) {
api.OpinionatedGET("/rooms/:room_id/messages", getChatHistory,
router.WithSummary("Get Chat History"),
router.WithTags("chat"))
api.OpinionatedGET("/rooms/:room_id/users", getRoomUsers,
router.WithSummary("Get Room Users"),
router.WithTags("chat"))
api.OpinionatedPOST("/rooms", createRoom,
router.WithSummary("Create Room"),
router.WithTags("chat"))
})
// Serve static files for chat UI
r.GET("/", func(w http.ResponseWriter, req *http.Request) {
http.ServeFile(w, req, "static/chat.html")
})
// Enable documentation
r.EnableOpenAPI()
r.EnableAsyncAPI()
log.Println("đź’¬ Chat server starting on :8080")
log.Println("📚 API docs at http://localhost:8080/openapi/docs")
log.Println("🔌 AsyncAPI docs at http://localhost:8080/asyncapi/docs")
log.Fatal(http.ListenAndServe(":8080", r))
}
func handleChatConnection(conn *router.WSConnection, message IncomingMessage) (*OutgoingMessage, error) {
roomID := conn.Param("room_id")
userID := conn.Request().URL.Query().Get("user_id")
username := conn.Request().URL.Query().Get("username")
if userID == "" || username == "" {
return nil, fmt.Errorf("user_id and username are required")
}
// Store user info in connection metadata
conn.SetMetadata("user_id", userID)
conn.SetMetadata("username", username)
conn.SetMetadata("room_id", roomID)
// Get or create room
room := roomManager.GetRoom(roomID)
if room == nil {
room = roomManager.CreateRoom(roomID, fmt.Sprintf("Room %s", roomID))
}
switch message.Type {
case "join":
return handleUserJoin(conn, room, userID, username)
case "message":
return handleChatMessage(conn, room, message)
case "leave":
return handleUserLeave(conn, room, userID)
case "typing":
return handleTypingIndicator(conn, room, message)
default:
return &OutgoingMessage{
Type: "error",
Error: "Unknown message type",
Timestamp: time.Now(),
}, nil
}
}
func handleChatMessage(conn *router.WSConnection, room *ChatRoom, msg IncomingMessage) (*OutgoingMessage, error) {
userID := conn.GetMetadata("user_id").(string)
username := conn.GetMetadata("username").(string)
// Create chat message
chatMsg := ChatMessage{
ID: generateMessageID(),
UserID: userID,
Username: username,
RoomID: room.ID,
Content: msg.Content,
Type: "text",
Timestamp: time.Now(),
}
// Store message
room.mu.Lock()
room.Messages = append(room.Messages, chatMsg)
room.mu.Unlock()
// Broadcast to all users in room
broadcastToRoom(room.ID, OutgoingMessage{
Type: "message",
Message: chatMsg,
RoomID: room.ID,
Timestamp: time.Now(),
}, conn.ClientID)
return &OutgoingMessage{
Type: "message_sent",
Message: chatMsg,
Timestamp: time.Now(),
}, nil
}
func broadcastToRoom(roomID string, message OutgoingMessage, excludeClientID string) {
cm := router.ConnectionManager()
for clientID, conn := range cm.WSConnections() {
if clientID == excludeClientID {
continue
}
if connRoomID, ok := conn.GetMetadata("room_id"); ok && connRoomID == roomID {
conn.SendMessage(router.WSMessage{
Type: "room_broadcast",
Payload: message,
})
}
}
}Task Management API with Real-time Updates
Project management API with WebSocket notifications:
package main
import (
"time"
router "github.com/xraph/steel"
)
// Task Models
type Task struct {
ID int `json:"id" description:"Task ID"`
Title string `json:"title" description:"Task title"`
Description string `json:"description" description:"Task description"`
Status string `json:"status" description:"Task status (todo, in_progress, done)"`
Priority string `json:"priority" description:"Task priority (low, medium, high)"`
AssigneeID *int `json:"assignee_id" description:"Assigned user ID"`
ProjectID int `json:"project_id" description:"Project ID"`
DueDate *time.Time `json:"due_date" description:"Task due date"`
Created time.Time `json:"created" description:"Creation timestamp"`
Updated time.Time `json:"updated" description:"Last update timestamp"`
}
type Project struct {
ID int `json:"id" description:"Project ID"`
Name string `json:"name" description:"Project name"`
Description string `json:"description" description:"Project description"`
OwnerID int `json:"owner_id" description:"Project owner user ID"`
Status string `json:"status" description:"Project status"`
Created time.Time `json:"created" description:"Creation timestamp"`
}
// Request Types
type CreateTaskRequest struct {
Title string `json:"title" body:"body" description:"Task title"`
Description string `json:"description" body:"body" description:"Task description"`
Priority string `json:"priority" body:"body" description:"Task priority"`
AssigneeID *int `json:"assignee_id" body:"body" description:"Assigned user ID"`
ProjectID int `path:"project_id" description:"Project ID"`
DueDate *time.Time `json:"due_date" body:"body" description:"Task due date"`
}
type UpdateTaskRequest struct {
ID int `path:"id" description:"Task ID"`
Title *string `json:"title" body:"body" description:"Task title"`
Description *string `json:"description" body:"body" description:"Task description"`
Status *string `json:"status" body:"body" description:"Task status"`
Priority *string `json:"priority" body:"body" description:"Task priority"`
AssigneeID *int `json:"assignee_id" body:"body" description:"Assigned user ID"`
DueDate *time.Time `json:"due_date" body:"body" description:"Task due date"`
}
type GetTasksRequest struct {
ProjectID int `path:"project_id" description:"Project ID"`
Status string `query:"status" description:"Filter by status"`
AssigneeID int `query:"assignee_id" description:"Filter by assignee"`
Page int `query:"page" description:"Page number"`
Limit int `query:"limit" description:"Items per page"`
}
// Real-time Update Types
type TaskUpdateNotification struct {
Type string `json:"type" description:"Update type (created, updated, deleted)"`
Task Task `json:"task" description:"Updated task"`
ProjectID int `json:"project_id" description:"Project ID"`
UserID int `json:"user_id" description:"User who made the change"`
Timestamp time.Time `json:"timestamp" description:"Update timestamp"`
}
func main() {
r := router.NewRouter()
// Middleware
r.Use(router.Logger)
r.Use(router.Recoverer)
r.Use(authMiddleware())
// Task API
r.Route("/api/v1/projects/:project_id", func(project router.Router) {
// Task CRUD
project.OpinionatedGET("/tasks", getTasks,
router.WithSummary("Get Project Tasks"),
router.WithDescription("Get paginated list of tasks for a project"),
router.WithTags("tasks"))
project.OpinionatedPOST("/tasks", createTask,
router.WithSummary("Create Task"),
router.WithDescription("Create a new task in the project"),
router.WithTags("tasks"))
project.OpinionatedGET("/tasks/:id", getTask,
router.WithSummary("Get Task"),
router.WithTags("tasks"))
project.OpinionatedPUT("/tasks/:id", updateTask,
router.WithSummary("Update Task"),
router.WithTags("tasks"))
project.OpinionatedDELETE("/tasks/:id", deleteTask,
router.WithSummary("Delete Task"),
router.WithTags("tasks"))
})
// Real-time updates
r.SSE("/sse/projects/:project_id/updates", handleProjectUpdates,
router.WithAsyncSummary("Project Updates"),
router.WithAsyncDescription("Real-time task and project updates"),
router.WithAsyncTags("tasks", "realtime", "sse"))
// Team collaboration WebSocket
r.WebSocket("/ws/projects/:project_id/collaborate", handleCollaboration,
router.WithAsyncSummary("Team Collaboration"),
router.WithAsyncDescription("Real-time team collaboration features"),
router.WithAsyncTags("collaboration", "websocket"))
r.EnableOpenAPI()
r.EnableAsyncAPI()
log.Fatal(http.ListenAndServe(":8080", r))
}
func createTask(ctx *router.Context, req CreateTaskRequest) (*Task, error) {
userID := getUserIDFromContext(ctx)
// Validate user has access to project
if !hasProjectAccess(userID, req.ProjectID) {
return nil, router.Forbidden("Access denied to project")
}
// Validate request
if req.Title == "" {
return nil, router.BadRequest("Title is required")
}
if req.Priority != "" && !isValidPriority(req.Priority) {
return nil, router.BadRequest("Invalid priority")
}
// Create task
task := &Task{
ID: generateTaskID(),
Title: req.Title,
Description: req.Description,
Status: "todo",
Priority: req.Priority,
AssigneeID: req.AssigneeID,
ProjectID: req.ProjectID,
DueDate: req.DueDate,
Created: time.Now(),
Updated: time.Now(),
}
// Save to database
if err := saveTask(task); err != nil {
return nil, router.InternalServerError("Failed to create task")
}
// Send real-time notification
notifyProjectUpdate(TaskUpdateNotification{
Type: "created",
Task: *task,
ProjectID: req.ProjectID,
UserID: userID,
Timestamp: time.Now(),
})
return task, nil
}
func updateTask(ctx *router.Context, req UpdateTaskRequest) (*Task, error) {
userID := getUserIDFromContext(ctx)
// Get existing task
task, err := getTaskByID(req.ID)
if err != nil {
return nil, router.NotFound("Task")
}
// Check permissions
if !hasTaskEditAccess(userID, task) {
return nil, router.Forbidden("Cannot edit this task")
}
// Update fields
if req.Title != nil {
task.Title = *req.Title
}
if req.Description != nil {
task.Description = *req.Description
}
if req.Status != nil {
if !isValidStatus(*req.Status) {
return nil, router.BadRequest("Invalid status")
}
task.Status = *req.Status
}
if req.Priority != nil {
if !isValidPriority(*req.Priority) {
return nil, router.BadRequest("Invalid priority")
}
task.Priority = *req.Priority
}
if req.AssigneeID != nil {
task.AssigneeID = req.AssigneeID
}
if req.DueDate != nil {
task.DueDate = req.DueDate
}
task.Updated = time.Now()
// Save changes
if err := saveTask(task); err != nil {
return nil, router.InternalServerError("Failed to update task")
}
// Send real-time notification
notifyProjectUpdate(TaskUpdateNotification{
Type: "updated",
Task: *task,
ProjectID: task.ProjectID,
UserID: userID,
Timestamp: time.Now(),
})
return task, nil
}
func handleProjectUpdates(conn *router.SSEConnection, params struct {
ProjectID int `path:"project_id" description:"Project ID"`
}) error {
userID := getUserIDFromRequest(conn.Request())
// Verify access to project
if !hasProjectAccess(userID, params.ProjectID) {
return fmt.Errorf("access denied to project")
}
// Store subscription
conn.SetMetadata("user_id", userID)
conn.SetMetadata("project_id", params.ProjectID)
// Send welcome message
conn.SendMessage(router.SSEMessage{
Event: "connected",
Data: map[string]interface{}{
"message": "Connected to project updates",
"project_id": params.ProjectID,
"user_id": userID,
},
})
// Keep connection alive and listen for updates
updateChan := subscribeToProjectUpdates(params.ProjectID, userID)
for {
select {
case update := <-updateChan:
err := conn.SendMessage(router.SSEMessage{
Event: "task_update",
Data: update,
})
if err != nil {
return err
}
case <-time.After(30 * time.Second):
// Send heartbeat
conn.SendMessage(router.SSEMessage{
Event: "heartbeat",
Data: map[string]interface{}{"time": time.Now()},
})
}
}
}
func notifyProjectUpdate(notification TaskUpdateNotification) {
cm := router.ConnectionManager()
// Notify SSE connections
for _, conn := range cm.SSEConnections() {
if projectID, ok := conn.GetMetadata("project_id"); ok {
if projectID.(int) == notification.ProjectID {
conn.SendMessage(router.SSEMessage{
Event: "task_update",
Data: notification,
})
}
}
}
// Notify WebSocket connections
for _, conn := range cm.WSConnections() {
if projectID, ok := conn.GetMetadata("project_id"); ok {
if projectID.(int) == notification.ProjectID {
conn.SendMessage(router.WSMessage{
Type: "task_update",
Payload: notification,
})
}
}
}
}File Upload and Processing API
API with file upload, processing, and progress tracking:
package main
import (
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
router "github.com/xraph/steel"
)
type FileUpload struct {
ID string `json:"id" description:"Upload ID"`
Filename string `json:"filename" description:"Original filename"`
Size int64 `json:"size" description:"File size in bytes"`
ContentType string `json:"content_type" description:"MIME type"`
Status string `json:"status" description:"Upload status"`
Progress float64 `json:"progress" description:"Processing progress (0-100)"`
URL string `json:"url,omitempty" description:"Download URL"`
Error string `json:"error,omitempty" description:"Error message"`
Created time.Time `json:"created" description:"Upload timestamp"`
Processed *time.Time `json:"processed,omitempty" description:"Processing completion time"`
}
type UploadProgressUpdate struct {
UploadID string `json:"upload_id" description:"Upload ID"`
Progress float64 `json:"progress" description:"Progress percentage"`
Status string `json:"status" description:"Current status"`
Message string `json:"message,omitempty" description:"Status message"`
}
func main() {
r := router.NewRouter()
r.Use(router.Logger)
r.Use(router.Recoverer)
// File upload endpoints
r.POST("/api/v1/upload", handleFileUpload)
r.OpinionatedGET("/api/v1/uploads/:id", getUploadStatus,
router.WithSummary("Get Upload Status"),
router.WithTags("uploads"))
r.OpinionatedGET("/api/v1/uploads", listUploads,
router.WithSummary("List Uploads"),
router.WithTags("uploads"))
// Real-time upload progress
r.SSE("/sse/uploads/:id/progress", handleUploadProgress,
router.WithAsyncSummary("Upload Progress"),
router.WithAsyncDescription("Real-time upload processing progress"),
router.WithAsyncTags("uploads", "progress", "sse"))
// File download
r.GET("/api/v1/uploads/:id/download", handleFileDownload)
r.EnableOpenAPI()
r.EnableAsyncAPI()
log.Fatal(http.ListenAndServe(":8080", r))
}
func handleFileUpload(w http.ResponseWriter, r *http.Request) {
// Parse multipart form (32MB max)
err := r.ParseMultipartForm(32 << 20)
if err != nil {
http.Error(w, "File too large", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "No file provided", http.StatusBadRequest)
return
}
defer file.Close()
// Validate file type
if !isAllowedFileType(header.Header.Get("Content-Type")) {
http.Error(w, "File type not allowed", http.StatusBadRequest)
return
}
// Create upload record
upload := &FileUpload{
ID: generateUploadID(),
Filename: header.Filename,
Size: header.Size,
ContentType: header.Header.Get("Content-Type"),
Status: "uploading",
Progress: 0,
Created: time.Now(),
}
// Save upload metadata
saveUpload(upload)
// Start async processing
go processFileUpload(upload, file)
// Return upload info
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(upload)
}
func processFileUpload(upload *FileUpload, file multipart.File) {
// Reset file position
file.Seek(0, 0)
// Create upload directory
uploadDir := filepath.Join("uploads", upload.ID)
os.MkdirAll(uploadDir, 0755)
// Save file
destPath := filepath.Join(uploadDir, upload.Filename)
destFile, err := os.Create(destPath)
if err != nil {
updateUploadStatus(upload.ID, "failed", 0, err.Error())
return
}
defer destFile.Close()
// Copy with progress tracking
updateUploadStatus(upload.ID, "processing", 10, "Saving file...")
_, err = io.Copy(destFile, file)
if err != nil {
updateUploadStatus(upload.ID, "failed", 0, err.Error())
return
}
updateUploadStatus(upload.ID, "processing", 50, "Processing file...")
// Process based on file type
switch upload.ContentType {
case "image/jpeg", "image/png", "image/gif":
processImage(upload, destPath)
case "application/pdf":
processPDF(upload, destPath)
case "text/csv":
processCSV(upload, destPath)
default:
// Just mark as completed for other types
updateUploadStatus(upload.ID, "completed", 100, "Upload completed")
}
}
func updateUploadStatus(uploadID, status string, progress float64, message string) {
// Update database
upload := getUploadByID(uploadID)
upload.Status = status
upload.Progress = progress
if status == "completed" {
now := time.Now()
upload.Processed = &now
upload.URL = fmt.Sprintf("/api/v1/uploads/%s/download", uploadID)
} else if status == "failed" {
upload.Error = message
}
saveUpload(upload)
// Send real-time update
notifyUploadProgress(UploadProgressUpdate{
UploadID: uploadID,
Progress: progress,
Status: status,
Message: message,
})
}
func handleUploadProgress(conn *router.SSEConnection, params struct {
ID string `path:"id" description:"Upload ID"`
}) error {
uploadID := params.ID
// Verify upload exists
upload := getUploadByID(uploadID)
if upload == nil {
return fmt.Errorf("upload not found")
}
// Store upload ID in connection
conn.SetMetadata("upload_id", uploadID)
// Send current status
conn.SendMessage(router.SSEMessage{
Event: "status",
Data: UploadProgressUpdate{
UploadID: uploadID,
Progress: upload.Progress,
Status: upload.Status,
},
})
// Keep connection alive
for !conn.IsClosed() {
time.Sleep(1 * time.Second)
// Check if upload is completed
currentUpload := getUploadByID(uploadID)
if currentUpload.Status == "completed" || currentUpload.Status == "failed" {
break
}
}
return nil
}
func notifyUploadProgress(update UploadProgressUpdate) {
cm := router.ConnectionManager()
for _, conn := range cm.SSEConnections() {
if uploadID, ok := conn.GetMetadata("upload_id"); ok {
if uploadID.(string) == update.UploadID {
conn.SendMessage(router.SSEMessage{
Event: "progress",
Data: update,
})
}
}
}
}These examples demonstrate real-world usage patterns with Steel, showing how to build production-ready APIs with authentication, real-time features, file handling, and comprehensive documentation.
Last updated on