Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ed61350
test: assert raw openapi spec matches fixture
gaby Aug 21, 2025
0cded2b
refactor: split route media types
gaby Aug 22, 2025
886ed13
feat: add route tags and deprecation
gaby Aug 22, 2025
60d4596
test: expand openapi middleware coverage
gaby Sep 4, 2025
149c053
test: cover openapi helpers
gaby Sep 4, 2025
ddbaa0f
feat(openapi): add operation helpers
gaby Sep 19, 2025
4ab58fd
refactor: use maps.Copy for metadata cloning
gaby Sep 21, 2025
9b1a022
docs: clarify openapi type reference
gaby Sep 21, 2025
2b4fa75
test: normalize openapi json assertions
gaby Oct 24, 2025
2bab831
chore: address openapi review feedback
gaby Oct 25, 2025
3c33f84
fix: scope openapi handler to its resolved path
gaby Nov 3, 2025
7da513d
Adjust OpenAPI default responses
gaby Dec 27, 2025
abe75b9
Extend OpenAPI helpers with schema refs and examples
gaby Jan 3, 2026
87d1f44
Merge branch 'main' into 2025-08-21-14-48-18
gaby Jan 9, 2026
868cf03
Merge branch 'main' into 2025-08-21-14-48-18
gaby Jan 16, 2026
ac3bc5c
Merge branch 'main' into 2025-08-21-14-48-18
gaby Jan 19, 2026
615d8e9
Merge branch 'main' into 2025-08-21-14-48-18
gaby Jan 21, 2026
abcc817
Merge branch 'main' into 2025-08-21-14-48-18
gaby Jan 23, 2026
603b4ab
Merge branch 'main' into 2025-08-21-14-48-18
gaby Jan 26, 2026
d092785
🐛 bug: fix merge conflicts and auto-generated HEAD routes in OpenAPI …
Claude Mar 27, 2026
0c16991
🔥 feat: merge parallel benchmarks from main into router_test.go
Claude Mar 27, 2026
272d61e
🧹 chore: fix lint issues and optimize struct alignment
Claude Mar 27, 2026
1ecf5d1
Merge branch 'main' into 2025-08-21-14-48-18
gaby Mar 27, 2026
b76e04d
🧹 chore: address review comments - clone Tags, filter middleware rout…
Copilot Mar 27, 2026
9158f56
🧹 chore: fix deprecated utils.ToLower lint warning
Copilot Mar 27, 2026
d342624
✅ test: add auto-HEAD exclusion test for OpenAPI middleware
Copilot Mar 27, 2026
4e33e7d
Merge branch 'main' into 2025-08-21-14-48-18
gaby Mar 29, 2026
49ee7a2
fix: add OpenAPI methods to domainRouter, defensive-copy Tags slice, …
Copilot Mar 29, 2026
38a32b9
refactor: simplify Tags() defensive copy
Copilot Mar 29, 2026
9946f64
🔒 security: harden OpenAPI middleware - fix path generation, add nil …
Claude Mar 30, 2026
8dcfd7c
🧪 test: improve openapi middleware coverage to 93.1%
Claude Mar 30, 2026
e3918d1
Merge branch 'main' into 2025-08-21-14-48-18
gaby Apr 1, 2026
057e9eb
Merge branch 'main' into 2025-08-21-14-48-18
gaby Apr 5, 2026
aa69482
Merge branch 'main' into 2025-08-21-14-48-18
gaby Apr 11, 2026
44772cb
Merge branch 'main' into 2025-08-21-14-48-18
gaby Apr 12, 2026
ff3e3ea
refactor(openapi): remove Operations config and rely on route helpers…
Copilot Apr 12, 2026
cc66f4b
test(domain): add OpenAPI helper coverage for domainRouter
Copilot Apr 13, 2026
d36e198
test(domain): refine OpenAPI helper test assertions
Copilot Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"errors"
"fmt"
"io"
"maps"
"mime"
"net"
"net/http"
"net/http/httputil"
Expand Down Expand Up @@ -706,6 +708,200 @@ func (app *App) Name(name string) Router {
return app
}

// Summary assigns a short summary to the most recently added route.
func (app *App) Summary(sum string) Router {
app.mutex.Lock()
app.latestRoute.Summary = sum
app.mutex.Unlock()
return app
}

// Description assigns a description to the most recently added route.
func (app *App) Description(desc string) Router {
app.mutex.Lock()
app.latestRoute.Description = desc
app.mutex.Unlock()
return app
}

// Consumes assigns a request media type to the most recently added route.
func (app *App) Consumes(typ string) Router {
if typ != "" {
if _, _, err := mime.ParseMediaType(typ); err != nil || !strings.Contains(typ, "/") {
panic("invalid media type: " + typ)
}
}
app.mutex.Lock()
app.latestRoute.Consumes = typ
app.mutex.Unlock()
return app
}

// Produces assigns a response media type to the most recently added route.
func (app *App) Produces(typ string) Router {
if typ != "" {
if _, _, err := mime.ParseMediaType(typ); err != nil || !strings.Contains(typ, "/") {
panic("invalid media type: " + typ)
}
}
app.mutex.Lock()
Comment thread
gaby marked this conversation as resolved.
app.latestRoute.Produces = typ
app.mutex.Unlock()
return app
}

// RequestBody documents the request payload for the most recently added route.
func (app *App) RequestBody(description string, required bool, mediaTypes ...string) Router {
sanitized := sanitizeRequiredMediaTypes(mediaTypes)

app.mutex.Lock()
app.latestRoute.RequestBody = &RouteRequestBody{
Description: description,
Required: required,
MediaTypes: append([]string(nil), sanitized...),
}
if len(sanitized) > 0 {
app.latestRoute.Consumes = sanitized[0]
}
app.mutex.Unlock()

return app
}

// Parameter documents an input parameter for the most recently added route.
func (app *App) Parameter(name, in string, required bool, schema map[string]any, description string) Router {
if strings.TrimSpace(name) == "" {
panic("parameter name is required")
}

location := strings.ToLower(strings.TrimSpace(in))
switch location {
case "path", "query", "header", "cookie":
default:
panic("invalid parameter location: " + in)
}

if schema == nil {
schema = map[string]any{"type": "string"}
}

schemaCopy := make(map[string]any, len(schema))
maps.Copy(schemaCopy, schema)
if _, ok := schemaCopy["type"]; !ok {
schemaCopy["type"] = "string"
}

if location == "path" {
required = true
}

param := RouteParameter{
Name: name,
In: location,
Required: required,
Description: description,
Schema: schemaCopy,
}

app.mutex.Lock()
app.latestRoute.Parameters = append(app.latestRoute.Parameters, param)
app.mutex.Unlock()

return app
}

// Response documents an HTTP response for the most recently added route.
func (app *App) Response(status int, description string, mediaTypes ...string) Router {
if status != 0 && (status < 100 || status > 599) {
panic("invalid status code")
}

sanitized := sanitizeMediaTypes(mediaTypes)

if description == "" {
if status == 0 {
description = "Default response"
} else if text := http.StatusText(status); text != "" {
description = text
} else {
description = "Status " + strconv.Itoa(status)
}
}

key := "default"
if status > 0 {
key = strconv.Itoa(status)
}

resp := RouteResponse{Description: description}
if len(sanitized) > 0 {
resp.MediaTypes = append([]string(nil), sanitized...)
}

app.mutex.Lock()
if app.latestRoute.Responses == nil {
app.latestRoute.Responses = make(map[string]RouteResponse)
}
app.latestRoute.Responses[key] = resp
if status == StatusOK && len(resp.MediaTypes) > 0 {
app.latestRoute.Produces = resp.MediaTypes[0]
}
app.mutex.Unlock()

return app
}

func sanitizeMediaTypes(mediaTypes []string) []string {
if len(mediaTypes) == 0 {
return nil
}

seen := make(map[string]struct{}, len(mediaTypes))
sanitized := make([]string, 0, len(mediaTypes))
for _, typ := range mediaTypes {
trimmed := strings.TrimSpace(typ)
if trimmed == "" {
continue
}
if _, _, err := mime.ParseMediaType(trimmed); err != nil || !strings.Contains(trimmed, "/") {
panic("invalid media type: " + typ)
Comment thread
gaby marked this conversation as resolved.
Outdated
}
if _, ok := seen[trimmed]; ok {
continue
}
seen[trimmed] = struct{}{}
sanitized = append(sanitized, trimmed)
}
if len(sanitized) == 0 {
return nil
}
return sanitized
}

func sanitizeRequiredMediaTypes(mediaTypes []string) []string {
sanitized := sanitizeMediaTypes(mediaTypes)
if len(sanitized) == 0 {
panic("at least one media type must be provided")
}
return sanitized
}

// Tags assigns tags to the most recently added route.
func (app *App) Tags(tags ...string) Router {
app.mutex.Lock()
app.latestRoute.Tags = tags
Comment thread
gaby marked this conversation as resolved.
Outdated
app.mutex.Unlock()
return app
}

// Deprecated marks the most recently added route as deprecated.
func (app *App) Deprecated() Router {
app.mutex.Lock()
app.latestRoute.Deprecated = true
app.mutex.Unlock()
return app
}

// GetRoute Get route by name
func (app *App) GetRoute(name string) Route {
for _, routes := range app.stack {
Expand Down
138 changes: 138 additions & 0 deletions docs/middleware/openapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
id: openapi
---

# OpenAPI

OpenAPI middleware for [Fiber](https://github.com/gofiber/fiber) that generates an OpenAPI specification based on the routes registered in your application.

## Signatures

```go
func New(config ...Config) fiber.Handler
```

## Examples

Import the middleware package that is part of the Fiber web framework

```go
import (
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/openapi"
)
```

After you initiate your Fiber app, you can use the following possibilities:

```go
// Initialize default config. Register the middleware *after* all routes
// so that the spec includes every handler.
Comment thread
gaby marked this conversation as resolved.
Outdated
app.Use(openapi.New())

// Or extend your config for customization
app.Use(openapi.New(openapi.Config{
Title: "My API",
Version: "1.0.0",
ServerURL: "https://example.com",
}))

// Customize metadata for specific operations
app.Use(openapi.New(openapi.Config{
Operations: map[string]openapi.Operation{
"GET /users": {
Summary: "List users",
Description: "Returns all users",
Produces: fiber.MIMEApplicationJSON,
},
},
}))
Comment thread
gaby marked this conversation as resolved.
Outdated

// Routes may optionally document themselves using Summary, Description,
// RequestBody, Parameter, Response, Tags, Deprecated, Produces and Consumes.
app.Post("/users", createUser).
Summary("Create user").
Description("Creates a new user").
RequestBody("User payload", true, fiber.MIMEApplicationJSON).
Parameter("trace-id", "header", true, nil, "Tracing identifier").
Response(fiber.StatusCreated, "Created", fiber.MIMEApplicationJSON).
Tags("users", "admin").
Produces(fiber.MIMEApplicationJSON)

// If not specified, routes default to an empty summary and description, no tags,
// not deprecated, and a "text/plain" request and response media type.
// Consumes and Produces will panic if provided an invalid media type.
```

Each documented route automatically includes a `200` response with the description `OK` to satisfy the minimum OpenAPI requirements. Additional responses can be declared via the `Response` helper or the middleware configuration.

`CONNECT` routes are ignored because the OpenAPI specification does not define a `connect` operation.

## Config

| Property | Type | Description | Default |
|:------------|:------------------------|:----------------------------------------------------------------|:------------------:|
| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
| Title | `string` | Title is the title for the generated OpenAPI specification. | `"Fiber API"` |
| Version | `string` | Version is the version for the generated OpenAPI specification. | `"1.0.0"` |
| Description | `string` | Description is the description for the generated specification. | `""` |
| ServerURL | `string` | ServerURL is the server URL used in the generated specification.| `""` |
| Path | `string` | Path is the route where the specification will be served. | `"/openapi.json"` |
| Operations | `map[string]Operation` | Per-route metadata keyed by `METHOD /path`. | `nil` |

## Default Config

```go
var ConfigDefault = Config{
Next: nil,
Operations: nil,
Title: "Fiber API",
Version: "1.0.0",
Description: "",
ServerURL: "",
Path: "/openapi.json",
}
```

### Operation

```go
type Operation struct {
RequestBody *RequestBody
Responses map[string]Response
Parameters []Parameter
Tags []string

ID string
Summary string
Description string
Consumes string
Produces string
Deprecated bool
}

type Parameter struct {
Schema map[string]any
Name string
In string
Description string
Required bool
}

type Media struct {
Schema map[string]any
}

type Response struct {
Content map[string]Media
Description string
}

type RequestBody struct {
Content map[string]Media
Description string
Required bool
}
```

Refer to the type definitions above when customizing OpenAPI operations in your configuration.
4 changes: 4 additions & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,10 @@ Deprecated fields `Duration`, `Store`, and `Key` have been removed in v3. Use `E

Monitor middleware is migrated to the [Contrib package](https://github.com/gofiber/contrib/tree/main/monitor) with [PR #1172](https://github.com/gofiber/contrib/pull/1172).

### OpenAPI

Introduces an `openapi` middleware that inspects registered routes and serves a generated OpenAPI 3.0 specification. Each operation includes a summary and default `200` response. Routes may attach descriptions, parameters, request bodies, and custom responses—alongside request/response media types—directly or configure them globally.

### Proxy

The proxy middleware has been updated to improve consistency with Go naming conventions. The `TlsConfig` field in the configuration struct has been renamed to `TLSConfig`. Additionally, the `WithTlsConfig` method has been removed; you should now configure TLS directly via the `TLSConfig` property within the `Config` struct.
Expand Down
Loading
Loading