Skip to content
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()
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: " + trimmed)
}
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
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
142 changes: 142 additions & 0 deletions docs/middleware/openapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
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.
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,
},
},
}))

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

When the middleware is attached to a group or mounted under a prefixed `Use`, the configured `Path` is resolved relative to that
prefix. For example, `app.Group("/v1").Use(openapi.New())` serves the specification at `/v1/openapi.json`, while a global `app.U
se(openapi.New())` only intercepts `/openapi.json` and will not affect other endpoints ending in `openapi.json`.

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