A Go library that extends Fiber to add automatic OpenAPI documentation generation with built-in validation and group support.
- âś… Complete HTTP methods (GET, POST, PUT, DELETE) with automatic validation
- âś… Group support with OpenAPI methods available on both app and groups
- âś… Unified API with interface-based approach for seamless app/group usage
- âś… Powerful validation via
github.com/go-playground/validator/v10 - âś… Authentication & Authorization with JWT/Bearer token support and role-based access control
- âś… OpenAPI Security documentation with automatic security scheme generation
- âś… Type safety with Go generics
- âś… Custom error handling
- âś… OpenAPI documentation generation with automatic schema generation
- âś… Redoc documentation UI for modern, responsive API documentation
- âś… Support for path, query, and body parameters
- âś… Automatic documentation setup with configurable paths
go get github.com/labbs/fiber-oapipackage main
import (
"github.com/gofiber/fiber/v2"
fiberoapi "github.com/labbs/fiber-oapi"
)
func main() {
app := fiber.New()
// Create OApi app with default configuration
// Documentation will be available at /documentation (Redoc UI) and /openapi.json
oapi := fiberoapi.New(app)
// Your routes here...
oapi.Listen(":3000")
}func main() {
app := fiber.New()
oapi := fiberoapi.New(app)
// Create groups with OpenAPI support
v1 := fiberoapi.Group(oapi, "/api/v1")
v2 := fiberoapi.Group(oapi, "/api/v2")
// Nested groups
users := fiberoapi.Group(v1, "/users")
admin := fiberoapi.Group(v1, "/admin")
// Routes work the same on app, groups, and nested groups
fiberoapi.Get(oapi, "/health", handler, options) // On app
fiberoapi.Get(v1, "/status", handler, options) // On group
fiberoapi.Post(users, "/", handler, options) // On nested group
oapi.Listen(":3000")
}func main() {
app := fiber.New()
// Custom configuration
config := fiberoapi.Config{
EnableValidation: true, // Enable input validation
EnableOpenAPIDocs: true, // Enable automatic docs setup
EnableAuthorization: false, // Enable authentication (default: false)
OpenAPIDocsPath: "/docs", // Custom docs path (default: /docs)
OpenAPIJSONPath: "/openapi.json", // Custom spec path (default: /openapi.json)
OpenAPIYamlPath: "/openapi.yaml", // Custom spec path (default: /openapi.yaml)
}
oapi := fiberoapi.New(app, config)
// Your routes here...
oapi.Listen(":3000")
}type GetInput struct {
Name string `path:"name" validate:"required,min=2"`
}
type GetOutput struct {
Message string `json:"message"`
}
type GetError struct {
Code int `json:"code"`
Message string `json:"message"`
}
// Works on app
fiberoapi.Get(oapi, "/greeting/:name",
func(c *fiber.Ctx, input GetInput) (GetOutput, GetError) {
return GetOutput{Message: "Hello " + input.Name}, GetError{}
},
fiberoapi.OpenAPIOptions{
OperationID: "get-greeting",
Tags: []string{"greeting"},
Summary: "Get a personalized greeting",
})
// Works on groups too
v1 := fiberoapi.Group(oapi, "/api/v1")
fiberoapi.Get(v1, "/greeting/:name", handler, options)type CreateUserInput struct {
Username string `json:"username" validate:"required,min=3,max=20,alphanum"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=13,max=120"`
}
type CreateUserOutput struct {
ID string `json:"id"`
Message string `json:"message"`
}
type CreateUserError struct {
Code int `json:"code"`
Message string `json:"message"`
}
fiberoapi.Post(oapi, "/users",
func(c *fiber.Ctx, input CreateUserInput) (CreateUserOutput, CreateUserError) {
if input.Username == "admin" {
return CreateUserOutput{}, CreateUserError{
Code: 403,
Message: "Username 'admin' is reserved",
}
}
return CreateUserOutput{
ID: "user_" + input.Username,
Message: "User created successfully",
}, CreateUserError{}
},
fiberoapi.OpenAPIOptions{
OperationID: "create-user",
Tags: []string{"users"},
Summary: "Create a new user",
})type UpdateUserInput struct {
ID string `path:"id" validate:"required"`
Username string `json:"username" validate:"omitempty,min=3,max=20,alphanum"`
Email string `json:"email" validate:"omitempty,email"`
Age int `json:"age" validate:"omitempty,min=13,max=120"`
}
type UpdateUserOutput struct {
ID string `json:"id"`
Message string `json:"message"`
Updated bool `json:"updated"`
}
fiberoapi.Put(oapi, "/users/:id",
func(c *fiber.Ctx, input UpdateUserInput) (UpdateUserOutput, CreateUserError) {
if input.ID == "notfound" {
return UpdateUserOutput{}, CreateUserError{
Code: 404,
Message: "User not found",
}
}
return UpdateUserOutput{
ID: input.ID,
Message: "User updated successfully",
Updated: true,
}, CreateUserError{}
},
fiberoapi.OpenAPIOptions{
OperationID: "update-user",
Tags: []string{"users"},
Summary: "Update an existing user",
})type DeleteUserInput struct {
ID string `path:"id" validate:"required"`
}
type DeleteUserOutput struct {
ID string `json:"id"`
Message string `json:"message"`
Deleted bool `json:"deleted"`
}
fiberoapi.Delete(oapi, "/users/:id",
func(c *fiber.Ctx, input DeleteUserInput) (DeleteUserOutput, CreateUserError) {
if input.ID == "protected" {
return DeleteUserOutput{}, CreateUserError{
Code: 403,
Message: "User is protected and cannot be deleted",
}
}
return DeleteUserOutput{
ID: input.ID,
Message: "User deleted successfully",
Deleted: true,
}, CreateUserError{}
},
fiberoapi.OpenAPIOptions{
OperationID: "delete-user",
Tags: []string{"users"},
Summary: "Delete a user",
})Fiber-oapi provides comprehensive authentication and authorization support with JWT/Bearer tokens, role-based access control, and automatic OpenAPI security documentation.
First, implement the AuthorizationService interface:
package main
import (
"fmt"
"time"
fiberoapi "github.com/labbs/fiber-oapi"
"github.com/gofiber/fiber/v2"
)
// Implement the AuthorizationService interface
type MyAuthService struct{}
func (s *MyAuthService) ValidateToken(token string) (*fiberoapi.AuthContext, error) {
// Validate your JWT token here
switch token {
case "admin-token":
return &fiberoapi.AuthContext{
UserID: "admin-123",
Roles: []string{"admin", "user"},
Scopes: []string{"read", "write", "delete"},
Claims: map[string]interface{}{
"sub": "admin-123",
"exp": time.Now().Add(time.Hour).Unix(),
},
}, nil
case "user-token":
return &fiberoapi.AuthContext{
UserID: "user-789",
Roles: []string{"user"},
Scopes: []string{"read", "write"},
Claims: map[string]interface{}{
"sub": "user-789",
"exp": time.Now().Add(time.Hour).Unix(),
},
}, nil
default:
return nil, fmt.Errorf("invalid token")
}
}
func (s *MyAuthService) HasRole(ctx *fiberoapi.AuthContext, role string) bool {
for _, r := range ctx.Roles {
if r == role {
return true
}
}
return false
}
func (s *MyAuthService) HasScope(ctx *fiberoapi.AuthContext, scope string) bool {
for _, sc := range ctx.Scopes {
if sc == scope {
return true
}
}
return false
}
func (s *MyAuthService) CanAccessResource(ctx *fiberoapi.AuthContext, resourceType, resourceID, action string) (bool, error) {
// Admins can do everything
if s.HasRole(ctx, "admin") {
return true, nil
}
// Custom resource-based logic
if resourceType == "document" && action == "delete" {
return false, nil // Only admins can delete
}
return s.HasScope(ctx, action), nil
}
func (s *MyAuthService) GetUserPermissions(ctx *fiberoapi.AuthContext, resourceType, resourceID string) (*fiberoapi.ResourcePermission, error) {
actions := []string{}
if s.HasScope(ctx, "read") {
actions = append(actions, "read")
}
if s.HasScope(ctx, "write") {
actions = append(actions, "write")
}
if s.HasRole(ctx, "admin") {
actions = append(actions, "delete")
}
return &fiberoapi.ResourcePermission{
ResourceType: resourceType,
ResourceID: resourceID,
Actions: actions,
}, nil
}
func main() {
app := fiber.New()
authService := &MyAuthService{}
// Configure with authentication
config := fiberoapi.Config{
EnableValidation: true,
EnableOpenAPIDocs: true,
EnableAuthorization: true,
AuthService: authService,
SecuritySchemes: map[string]fiberoapi.SecurityScheme{
"bearerAuth": {
Type: "http",
Scheme: "bearer",
BearerFormat: "JWT",
Description: "JWT Bearer token",
},
},
DefaultSecurity: []map[string][]string{
{"bearerAuth": {}},
},
}
oapi := fiberoapi.New(app, config)
// Your authenticated routes here...
oapi.Listen(":3000")
}// Public route (no authentication required)
fiberoapi.Get(oapi, "/health",
func(c *fiber.Ctx, input struct{}) (map[string]string, *fiberoapi.ErrorResponse) {
return map[string]string{"status": "ok"}, nil
},
fiberoapi.OpenAPIOptions{
Summary: "Health check",
Security: "disabled", // Explicitly disable auth for this route
})
// Protected route (authentication required by default)
fiberoapi.Get(oapi, "/profile",
func(c *fiber.Ctx, input struct{}) (UserProfile, *fiberoapi.ErrorResponse) {
// Get authenticated user context
authCtx, err := fiberoapi.GetAuthContext(c)
if err != nil {
return UserProfile{}, &fiberoapi.ErrorResponse{
Code: 401,
Details: "Authentication required",
Type: "auth_error",
}
}
return UserProfile{
UserID: authCtx.UserID,
Roles: authCtx.Roles,
}, nil
},
fiberoapi.OpenAPIOptions{
Summary: "Get user profile",
Tags: []string{"user"},
})type DocumentRequest struct {
DocumentID string `path:"documentId" validate:"required"`
}
type DocumentResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
}
// Route requiring specific role
fiberoapi.Get(oapi, "/documents/:documentId",
func(c *fiber.Ctx, input DocumentRequest) (DocumentResponse, *fiberoapi.ErrorResponse) {
authCtx, _ := fiberoapi.GetAuthContext(c)
// Check if user has required role
if !authService.HasRole(authCtx, "user") {
return DocumentResponse{}, &fiberoapi.ErrorResponse{
Code: 403,
Details: "User role required",
Type: "authorization_error",
}
}
// Check if user has required scope
if !authService.HasScope(authCtx, "read") {
return DocumentResponse{}, &fiberoapi.ErrorResponse{
Code: 403,
Details: "Read permission required",
Type: "authorization_error",
}
}
return DocumentResponse{
ID: input.DocumentID,
Title: "Document Title",
Content: "Document content",
}, nil
},
fiberoapi.OpenAPIOptions{
Summary: "Get document",
Description: "Requires 'user' role and 'read' scope",
Tags: []string{"documents"},
})
// Admin-only route
fiberoapi.Delete(oapi, "/documents/:documentId",
func(c *fiber.Ctx, input DocumentRequest) (map[string]bool, *fiberoapi.ErrorResponse) {
authCtx, _ := fiberoapi.GetAuthContext(c)
// Only admins can delete
if !authService.HasRole(authCtx, "admin") {
return nil, &fiberoapi.ErrorResponse{
Code: 403,
Details: "Admin role required",
Type: "authorization_error",
}
}
return map[string]bool{"deleted": true}, nil
},
fiberoapi.OpenAPIOptions{
Summary: "Delete document",
Description: "Admin only - requires 'admin' role",
Tags: []string{"documents", "admin"},
})Use the helper functions to simplify security configuration:
// Add security to existing options
options := fiberoapi.OpenAPIOptions{Summary: "Protected endpoint"}
options = fiberoapi.WithSecurity(options, map[string][]string{
"bearerAuth": {},
})
// Disable security for specific route
options = fiberoapi.WithSecurityDisabled(options)
// Add required permissions for documentation
options = fiberoapi.WithPermissions(options, "document:read", "workspace:admin")
// Set resource type for documentation
options = fiberoapi.WithResourceType(options, "document")Access the authenticated user's information in your handlers:
fiberoapi.Post(oapi, "/posts",
func(c *fiber.Ctx, input CreatePostInput) (PostResponse, *fiberoapi.ErrorResponse) {
// Get authenticated user context
authCtx, err := fiberoapi.GetAuthContext(c)
if err != nil {
return PostResponse{}, &fiberoapi.ErrorResponse{
Code: 401,
Details: "Authentication required",
Type: "auth_error",
}
}
// Use auth context
fmt.Printf("User %s with roles %v creating post\n", authCtx.UserID, authCtx.Roles)
// Access JWT claims if needed
if exp, ok := authCtx.Claims["exp"].(int64); ok {
if time.Now().Unix() > exp {
return PostResponse{}, &fiberoapi.ErrorResponse{
Code: 401,
Details: "Token expired",
Type: "auth_error",
}
}
}
return PostResponse{
ID: "post-123",
Author: authCtx.UserID,
Title: input.Title,
}, nil
},
fiberoapi.OpenAPIOptions{
Summary: "Create a post",
Tags: []string{"posts"},
})When authentication is enabled, the OpenAPI specification automatically includes:
- Security Schemes: JWT Bearer token configuration
- Security Requirements: Applied to protected endpoints
- Security Overrides: Public endpoints marked with
security: []
{
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT Bearer token"
}
}
},
"security": [
{"bearerAuth": []}
],
"paths": {
"/health": {
"get": {
"security": [],
"summary": "Health check"
}
},
"/profile": {
"get": {
"security": [{"bearerAuth": []}],
"summary": "Get user profile"
}
}
}
}# Public endpoint
curl http://localhost:3000/health
# Protected endpoint
curl -H "Authorization: Bearer your-jwt-token" http://localhost:3000/profile
# Admin endpoint
curl -H "Authorization: Bearer admin-token" -X DELETE http://localhost:3000/documents/123Authentication errors are automatically formatted:
{
"code": 401,
"details": "Invalid token",
"type": "auth_error"
}{
"code": 403,
"details": "Admin role required",
"type": "authorization_error"
}See the complete working example in _examples/auth/main.go which demonstrates:
- Multiple user types with different roles and scopes
- Role-based access control
- Scope-based permissions
- Resource-level authorization
- OpenAPI security documentation
- Public and protected endpoints
// Clone the repository and run the auth example
go run _examples/auth/main.go
// Visit http://localhost:3002/docs to see the documentation
// Test with different tokens: admin-token, user-token, readonly-token
## Configuration
The library supports flexible configuration through the `Config` struct:
```go
type Config struct {
EnableValidation bool // Enable/disable input validation (default: true)
EnableOpenAPIDocs bool // Enable automatic docs setup (default: true)
EnableAuthorization bool // Enable authentication/authorization (default: false)
OpenAPIDocsPath string // Path for documentation UI (default: "/docs")
OpenAPIJSONPath string // Path for OpenAPI JSON spec (default: "/openapi.json")
AuthService AuthorizationService // Service for handling auth
SecuritySchemes map[string]SecurityScheme // OpenAPI security schemes
DefaultSecurity []map[string][]string // Default security requirements
}
If no configuration is provided, the library uses these defaults:
- Validation: enabled
- Documentation: enabled
- Authorization: disabled
- Docs path:
/docs - JSON spec path:
/openapi.json
// Disable documentation but keep validation
config := fiberoapi.Config{
EnableValidation: true,
EnableOpenAPIDocs: false,
}
// Or disable validation but keep docs
config := fiberoapi.Config{
EnableValidation: false,
EnableOpenAPIDocs: true,
OpenAPIDocsPath: "/api-docs",
OpenAPIJSONPath: "/openapi.json",
}
// Enable authentication with custom paths
config := fiberoapi.Config{
EnableValidation: true,
EnableOpenAPIDocs: true,
EnableAuthorization: true,
AuthService: &MyAuthService{},
OpenAPIDocsPath: "/documentation",
OpenAPIJSONPath: "/openapi.json",
}This library uses validator/v10 for validation. You can use all supported validation tags:
required- Required fieldmin=3,max=20- Min/max lengthemail- Valid email formatalphanum- Alphanumeric characters onlyuuid4- UUID version 4url- Valid URLoneof=admin user guest- Value from a listdive- Validation for slice elementsgtfield=MinPrice- Greater than another field
- Path parameters:
path:"paramName"(GET, POST, PUT, DELETE) - Query parameters:
query:"paramName"(GET, DELETE) - JSON body:
json:"fieldName"(POST, PUT)
All methods work with both the main app and groups through the unified API:
- GET:
fiberoapi.Get()- Retrieve resources with path/query parameters - POST:
fiberoapi.Post()- Create resources with JSON body + optional path parameters - PUT:
fiberoapi.Put()- Update resources with path parameters + JSON body - DELETE:
fiberoapi.Delete()- Delete resources with path parameters + optional query parameters
For backward compatibility, the old method names are still available:
fiberoapi.GetOApi()fiberoapi.PostOApi()fiberoapi.PutOApi()fiberoapi.DeleteOApi()
Fiber-oapi provides full support for Fiber groups while maintaining access to OpenAPI methods:
// Create the main app
app := fiber.New()
oapi := fiberoapi.New(app)
// Create groups - they have access to all Fiber Router methods AND OpenAPI methods
v1 := fiberoapi.Group(oapi, "/api/v1")
v2 := fiberoapi.Group(oapi, "/api/v2")
// Nested groups work too
users := fiberoapi.Group(v1, "/users")
admin := fiberoapi.Group(v1, "/admin")
// Use OpenAPI methods on any router (app or group)
fiberoapi.Get(oapi, "/health", healthHandler, options) // Main app
fiberoapi.Get(v1, "/status", statusHandler, options) // Group
fiberoapi.Post(users, "/", createUserHandler, options) // Nested group
fiberoapi.Put(users, "/:id", updateUserHandler, options) // Nested group
// Use standard Fiber Router methods on groups (inherited via embedding)
v1.Use("/protected", authMiddleware) // Middleware
admin.Get("/stats", func(c *fiber.Ctx) error { // Regular Fiber handler
return c.JSON(fiber.Map{"stats": "data"})
})
// For static files, use the main Fiber app
app.Static("/files", "./uploads") // Static files via main app
// Groups preserve full path context for OpenAPI documentation
// fiberoapi.Get(users, "/:id", ...) registers as GET /api/v1/users/{id}- Fiber Router compatibility: Groups embed
fiber.Routerso standard Router methods work (Use, Get, Post, etc.) - OpenAPI method support: Use
fiberoapi.Get(),fiberoapi.Post(), etc. on groups - Nested groups: Create groups within groups with proper path handling
- Path prefix handling: OpenAPI paths are automatically constructed with full prefixes
- Unified API: Same function names work on both app and groups through interface polymorphism
Note: For features like static file serving, use the main Fiber app: app.Static("/path", "./dir")
Validation errors are automatically formatted and returned with HTTP status 400:
{
"error": "Validation failed",
"details": "Key: 'CreateUserInput.Username' Error:Field validation for 'Username' failed on the 'min' tag"
}Custom errors use the StatusCode from your error struct.
Run tests:
go test -vpackage main
import (
"github.com/gofiber/fiber/v2"
fiberoapi "github.com/labbs/fiber-oapi"
)
type UserInput struct {
ID int `path:"id" validate:"required,min=1"`
}
type UserOutput struct {
ID int `json:"id"`
Name string `json:"name"`
}
type UserError struct {
Message string `json:"message"`
}
func main() {
app := fiber.New()
oapi := fiberoapi.New(app)
// Global routes
fiberoapi.Get(oapi, "/health", func(c *fiber.Ctx, input struct{}) (map[string]string, struct{}) {
return map[string]string{"status": "ok"}, struct{}{}
}, fiberoapi.OpenAPIOptions{
Summary: "Health check",
Tags: []string{"health"},
})
// API v1 group
v1 := fiberoapi.Group(oapi, "/api/v1")
fiberoapi.Get(v1, "/users/:id", func(c *fiber.Ctx, input UserInput) (UserOutput, UserError) {
return UserOutput{ID: input.ID, Name: "User " + string(rune(input.ID))}, UserError{}
}, fiberoapi.OpenAPIOptions{
Summary: "Get user by ID",
Tags: []string{"users"},
})
fiberoapi.Post(v1, "/users", func(c *fiber.Ctx, input UserOutput) (UserOutput, UserError) {
return UserOutput{ID: 99, Name: input.Name}, UserError{}
}, fiberoapi.OpenAPIOptions{
Summary: "Create a new user",
Tags: []string{"users"},
})
// API v2 with nested groups
v2 := fiberoapi.Group(oapi, "/api/v2")
usersV2 := fiberoapi.Group(v2, "/users")
fiberoapi.Get(usersV2, "/:id", func(c *fiber.Ctx, input UserInput) (UserOutput, UserError) {
return UserOutput{ID: input.ID, Name: "User v2 " + string(rune(input.ID))}, UserError{}
}, fiberoapi.OpenAPIOptions{
Summary: "Get user by ID (v2)",
Tags: []string{"users", "v2"},
})
// Mix with standard Fiber methods
v1.Use("/admin", func(c *fiber.Ctx) error {
return c.Next() // Auth middleware
})
app.Listen(":3000")
// Visit http://localhost:3000/docs to see the Redoc documentation
}config := fiberoapi.DocConfig{
Title: "My API",
Description: "My API description",
Version: "2.0.0",
DocsPath: "/documentation",
JSONPath: "/api-spec.json",
}
oapi.SetupDocs(config) // Optional - docs are auto-configured by defaultThe library uses an OApiRouter interface that allows the same functions to work seamlessly with both apps and groups:
// This interface is implemented by both *OApiApp and *OApiGroup
type OApiRouter interface {
GetApp() *OApiApp
GetPrefix() string
}
// So these functions work with both:
func Get[T any, U any, E any](router OApiRouter, path string, handler HandlerFunc[T, U, E], options OpenAPIOptions)
func Post[T any, U any, E any](router OApiRouter, path string, handler HandlerFunc[T, U, E], options OpenAPIOptions)
func Put[T any, U any, E any](router OApiRouter, path string, handler HandlerFunc[T, U, E], options OpenAPIOptions)
func Delete[T any, U any, E any](router OApiRouter, path string, handler HandlerFunc[T, U, E], options OpenAPIOptions)
func Group(router OApiRouter, prefix string, handlers ...fiber.Handler) *OApiGroupWhen EnableOpenAPIDocs is set to true (default), the library automatically sets up:
- Redoc UI: Modern, responsive documentation interface available at the configured docs path (default:
/docs) - OpenAPI JSON: Complete OpenAPI 3.0 specification available at the configured JSON path (default:
/openapi.json) - Automatic Schema Generation: Input and output types are automatically converted to OpenAPI schemas
- Components Section: All schemas are properly organized in the
components/schemassection
No manual setup required! Just visit http://localhost:3000/docs to see your API documentation with Redoc.
This library uses Redoc for documentation UI instead of Swagger UI because:
- Better performance with large APIs
- Responsive design that works great on mobile
- Clean, modern interface
- Better OpenAPI 3.0 support
- No JavaScript framework dependencies
The library automatically generates OpenAPI 3.0 schemas from your Go types:
// This struct automatically becomes an OpenAPI schema
type User struct {
ID string `json:"id"`
Username string `json:"username" validate:"required,min=3"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"min=13,max=120"`
}Generated OpenAPI spec will include:
- Complete path definitions with parameters
- Request/response schemas
- Validation rules as schema constraints
- Proper HTTP status codes
- Operation IDs, tags, and descriptions
If you're migrating from a previous version, here are the key changes:
fiberoapi.Get(oapi, "/users/:id", handler, options) // Works on app
fiberoapi.Post(oapi, "/users", handler, options) // Works on app
// And seamlessly on groups
v1 := fiberoapi.Group(oapi, "/api/v1")
fiberoapi.Get(v1, "/users/:id", handler, options) // Works on groups
fiberoapi.Post(v1, "/users", handler, options) // Works on groups// New group functionality
v1 := fiberoapi.Group(oapi, "/api/v1")
users := fiberoapi.Group(v1, "/users")
// All OpenAPI methods work on groups
fiberoapi.Get(users, "/:id", getUserHandler, options)
fiberoapi.Post(users, "/", createUserHandler, options)
// Standard Fiber Router methods work too (inherited via embedding)
users.Use(authMiddleware) // Middleware
users.Get("/legacy", func(c *fiber.Ctx) error { // Regular Fiber handler
return c.SendString("legacy endpoint")
})
// For static files, use the main Fiber app
app.Static("/avatars", "./uploads") // Static files via main app- Changed from Swagger UI to Redoc for better performance and modern UI
- Same paths:
/docsfor UI,/openapi.jsonfor spec - No code changes required for existing documentation setup