Overview
API handlers are defined inpkg/server/handlers/ and routes are registered in pkg/server/server.go.
Handler Structure
Copy
// pkg/server/handlers/my_handler.go
package handlers
import (
"github.com/gofiber/fiber/v2"
"github.com/osmedeus/osmedeus-ng/internal/config"
)
// MyHandler handles GET /osm/api/my-endpoint
func MyHandler(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
// Your logic here
return c.JSON(fiber.Map{
"data": "result",
})
}
}
Steps to Add an Endpoint
1. Create Handler File
Createpkg/server/handlers/my_handler.go:
Copy
package handlers
import (
"github.com/gofiber/fiber/v2"
"github.com/osmedeus/osmedeus-ng/internal/config"
"github.com/osmedeus/osmedeus-ng/internal/database"
)
// Request/Response types
type MyRequest struct {
Name string `json:"name" validate:"required"`
Value int `json:"value"`
Option string `json:"option,omitempty"`
}
type MyResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Message string `json:"message,omitempty"`
}
// ListMyItems handles GET /osm/api/my-items
func ListMyItems(cfg *config.Config, db *database.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
// Parse query parameters
limit := c.QueryInt("limit", 20)
offset := c.QueryInt("offset", 0)
filter := c.Query("filter", "")
// Query database
items, total, err := db.ListItems(limit, offset, filter)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": fiber.Map{
"code": "DATABASE_ERROR",
"message": err.Error(),
},
})
}
return c.JSON(fiber.Map{
"data": items,
"meta": fiber.Map{
"total": total,
"limit": limit,
"offset": offset,
},
})
}
}
// GetMyItem handles GET /osm/api/my-items/:id
func GetMyItem(cfg *config.Config, db *database.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
id := c.Params("id")
item, err := db.GetItemByID(id)
if err != nil {
return c.Status(404).JSON(fiber.Map{
"error": fiber.Map{
"code": "NOT_FOUND",
"message": "Item not found",
},
})
}
return c.JSON(fiber.Map{
"data": item,
})
}
}
// CreateMyItem handles POST /osm/api/my-items
func CreateMyItem(cfg *config.Config, db *database.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
var req MyRequest
// Parse body
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{
"error": fiber.Map{
"code": "INVALID_REQUEST",
"message": "Invalid request body",
},
})
}
// Validate
if req.Name == "" {
return c.Status(400).JSON(fiber.Map{
"error": fiber.Map{
"code": "VALIDATION_ERROR",
"message": "name is required",
},
})
}
// Create item
item, err := db.CreateItem(req.Name, req.Value, req.Option)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": fiber.Map{
"code": "CREATE_FAILED",
"message": err.Error(),
},
})
}
return c.Status(201).JSON(fiber.Map{
"data": MyResponse{
ID: item.ID,
Name: item.Name,
Status: "created",
},
})
}
}
// UpdateMyItem handles PUT /osm/api/my-items/:id
func UpdateMyItem(cfg *config.Config, db *database.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
id := c.Params("id")
var req MyRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{
"error": fiber.Map{
"code": "INVALID_REQUEST",
"message": "Invalid request body",
},
})
}
item, err := db.UpdateItem(id, req.Name, req.Value)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": fiber.Map{
"code": "UPDATE_FAILED",
"message": err.Error(),
},
})
}
return c.JSON(fiber.Map{
"data": item,
})
}
}
// DeleteMyItem handles DELETE /osm/api/my-items/:id
func DeleteMyItem(cfg *config.Config, db *database.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
id := c.Params("id")
if err := db.DeleteItem(id); err != nil {
return c.Status(500).JSON(fiber.Map{
"error": fiber.Map{
"code": "DELETE_FAILED",
"message": err.Error(),
},
})
}
return c.JSON(fiber.Map{
"message": "Item deleted",
})
}
}
2. Register Routes
Updatepkg/server/server.go:
Copy
func (s *Server) setupRoutes() {
// ... existing routes ...
// My Items routes
api.Get("/my-items", handlers.ListMyItems(s.config, s.db))
api.Get("/my-items/:id", handlers.GetMyItem(s.config, s.db))
api.Post("/my-items", handlers.CreateMyItem(s.config, s.db))
api.Put("/my-items/:id", handlers.UpdateMyItem(s.config, s.db))
api.Delete("/my-items/:id", handlers.DeleteMyItem(s.config, s.db))
}
3. Add Swagger Documentation
Add comments for swagger generation:Copy
// ListMyItems godoc
// @Summary List items
// @Description Get list of items with pagination
// @Tags my-items
// @Accept json
// @Produce json
// @Param limit query int false "Limit" default(20)
// @Param offset query int false "Offset" default(0)
// @Param filter query string false "Filter"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /osm/api/my-items [get]
// @Security BearerAuth
func ListMyItems(cfg *config.Config, db *database.DB) fiber.Handler {
// ...
}
Copy
make swagger
4. Write Tests
Createpkg/server/handlers/my_handler_test.go:
Copy
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestListMyItems(t *testing.T) {
app := fiber.New()
cfg := testConfig()
db := testDB()
app.Get("/api/my-items", ListMyItems(cfg, db))
req := httptest.NewRequest("GET", "/api/my-items?limit=10", nil)
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
assert.Contains(t, result, "data")
assert.Contains(t, result, "meta")
}
func TestCreateMyItem(t *testing.T) {
app := fiber.New()
cfg := testConfig()
db := testDB()
app.Post("/api/my-items", CreateMyItem(cfg, db))
body := `{"name": "test", "value": 42}`
req := httptest.NewRequest("POST", "/api/my-items", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
}
func TestCreateMyItem_ValidationError(t *testing.T) {
app := fiber.New()
cfg := testConfig()
db := testDB()
app.Post("/api/my-items", CreateMyItem(cfg, db))
body := `{"value": 42}` // Missing required name
req := httptest.NewRequest("POST", "/api/my-items", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}
Common Patterns
File Upload
Copy
func UploadFile(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
file, err := c.FormFile("file")
if err != nil {
return c.Status(400).JSON(fiber.Map{
"error": "No file uploaded",
})
}
// Save file
path := filepath.Join(cfg.UploadDir, file.Filename)
if err := c.SaveFile(file, path); err != nil {
return c.Status(500).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.JSON(fiber.Map{
"path": path,
"size": file.Size,
})
}
}
Streaming Response
Copy
func StreamData(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
c.Set("Content-Type", "text/event-stream")
c.Set("Cache-Control", "no-cache")
c.Set("Connection", "keep-alive")
c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
for i := 0; i < 10; i++ {
fmt.Fprintf(w, "data: Event %d\n\n", i)
w.Flush()
time.Sleep(time.Second)
}
})
return nil
}
}
WebSocket
Copy
import "github.com/gofiber/websocket/v2"
func WebSocketHandler() fiber.Handler {
return websocket.New(func(c *websocket.Conn) {
for {
mt, msg, err := c.ReadMessage()
if err != nil {
break
}
// Echo message back
if err := c.WriteMessage(mt, msg); err != nil {
break
}
}
})
}
Response Format
Success Response
Copy
{
"data": { ... },
"meta": {
"total": 100,
"limit": 20,
"offset": 0
}
}
Error Response
Copy
{
"error": {
"code": "ERROR_CODE",
"message": "Human readable message"
}
}
Best Practices
- Use consistent response format
- Return appropriate HTTP status codes
- Validate input early
- Add Swagger documentation
- Write handler tests
- Use dependency injection for config and db
Next Steps
- Adding CLI Commands - CLI extensions
- API Overview - Existing endpoints
- API Quick Reference - All endpoints