Skip to main content
Add custom REST API endpoints to the Osmedeus server.

Overview

API handlers are defined in pkg/server/handlers/ and routes are registered in pkg/server/server.go.

Handler Structure

// 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

Create pkg/server/handlers/my_handler.go:
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

Update pkg/server/server.go:
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:
// 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 {
    // ...
}
Generate swagger:
make swagger

4. Write Tests

Create pkg/server/handlers/my_handler_test.go:
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

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

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

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

{
  "data": { ... },
  "meta": {
    "total": 100,
    "limit": 20,
    "offset": 0
  }
}

Error Response

{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human readable message"
  }
}

Best Practices

  1. Use consistent response format
  2. Return appropriate HTTP status codes
  3. Validate input early
  4. Add Swagger documentation
  5. Write handler tests
  6. Use dependency injection for config and db

Next Steps