Built-in Middleware in Go with NestGo

NestGo ships with 14 production-grade middleware in the github.com/ashrafAli23/nestgo/middleware package, covering everything from panic recovery and rate limiting to CSRF protection and gzip compression in Go. Every middleware works identically on the Gin and Fiber adapters, follows the same Xxx(config ...XxxConfig) pattern with sensible defaults, and returns a core.MiddlewareFunc you can apply globally or per route.

How to apply middleware in NestGo

Register middleware globally on the server, or attach them to a single route or group:

import (
    core "github.com/ashrafAli23/nestgo/core"
    "github.com/ashrafAli23/nestgo/middleware"
)

// Global — runs for every request
server.Use(middleware.Recovery())
server.Use(middleware.Logger())

// Per-route — pass as extra arguments after the handler
r.POST("/upload", handler, middleware.Timeout(middleware.TimeoutConfig{
    Timeout: 60 * time.Second,
}))

Every middleware accepts zero or one config struct. Calling it with no arguments uses the defaults documented below. In a custom config, zero-valued string, numeric, and function fields fall back to the same defaults — but boolean fields (ETagConfig.Weak, CSRFConfig.CookieHTTPOnly, RateLimitConfig.Headers, TracingConfig.RecordDuration) and the Helmet/CORS header lists are taken as-is, so restate any you want to keep.

Order matters — Recovery must wrap everything, and correlation IDs should exist before the Logger reads them:

server.Use(middleware.Recovery())   // first: catch panics from everything below
server.Use(middleware.RequestID())  // correlation ID before logging
server.Use(middleware.Tracing())    // trace_id before logging
server.Use(middleware.Logger())     // logs pick up request_id + trace_id
server.Use(middleware.Helmet())     // security headers
server.Use(middleware.CORS(middleware.CORSConfig{
    AllowOrigins: []string{"https://app.example.com"},
}))
server.Use(middleware.RateLimit())
server.Use(middleware.Timeout())

Middleware summary table

Middleware Purpose Key default
Recovery Recover from handler panics Generic 500 response; panic details only logged
Logger Log method, path, status, duration Logs via core.Log()
RequestID Attach a unique ID to every request X-Request-ID header
Tracing W3C trace context propagation Reads traceparent, echoes X-Trace-ID
Helmet OWASP security headers 8 headers on by default
CORS Cross-origin resource sharing * origins (dev-friendly)
RateLimit Fixed-window rate limiting 100 req/min per client IP
Timeout Deadline on the request context 30s, 504 on expiry
BodyLimit Reject oversized request bodies 4MB, 413 on excess
CSRF Double-submit cookie CSRF protection _csrf cookie + X-CSRF-Token header
Compress Gzip compression for JSON responses Default level, 1KB minimum
ETag ETag + If-None-Match → 304 Weak ETags, SHA-256 based
Idempotency Replay cached responses by key Idempotency-Key, 24h TTL
Upload Validate multipart file uploads 10MB max, file field

Recovery — Panic Recovery Middleware in Go

middleware.Recovery() catches panics in handlers and turns them into HTTP error responses instead of crashing the whole Go server. Register it first so it wraps every other middleware and handler.

If a handler panics with a *core.HTTPError, Recovery preserves its status code and message — those are intentional, client-facing errors. Every other panic value (including plain errors) becomes a generic 500 “Internal Server Error”: the panic message and stack are logged, never sent to the client, unless you opt in with ExposeError.

Defaults (DefaultRecoveryConfig()):

Field Type Default Description
StackSize int 8192 Max stack trace size in bytes captured per panic
EnableStackTrace bool false Include the stack trace in the error response details
ExposeError bool false Include the panic value’s message in the client response — dev only; when false, clients get a generic “Internal Server Error”
LogFunc func(c core.Context, err interface{}, stack string) logs via core.Log().Error Called when a panic is recovered
ErrorHandler func(c core.Context, recovered interface{}, stack string) error nil If set, takes full control of the error response
server.Use(middleware.Recovery())

// Custom error response format
server.Use(middleware.Recovery(middleware.RecoveryConfig{
    ErrorHandler: func(c core.Context, recovered interface{}, stack string) error {
        return c.JSON(500, map[string]any{
            "error": "something went wrong",
            "code":  "INTERNAL",
        })
    },
}))

Production notes

  • Keep EnableStackTrace: false in production — stack traces in responses leak file paths and internals. The stack is always available to LogFunc regardless.
  • Keep ExposeError: false in production too — panic messages routinely contain internal details (DSNs, file paths, hosts). The full panic value and stack always reach LogFunc either way, so nothing is lost from your logs.
  • Stack buffers are pooled with sync.Pool, so the panic path costs a single string allocation when stack traces are disabled.

Logger — Request Logging Middleware

middleware.Logger() logs every request with method, path, status, duration, and client IP through NestGo’s structured logger. When RequestID or Tracing run before it, the request_id and trace_id fields are included automatically.

Config (LoggerConfig — no defaults constructor; the zero value logs via core.Log()):

Field Type Default Description
SkipFunc func(c core.Context) bool nil Return true to skip logging (e.g. health checks)
LogFunc func(c core.Context, status int, duration time.Duration, err error) nil (uses core.Log()) Override the log output entirely
// Skip health checks
server.Use(middleware.Logger(middleware.LoggerConfig{
    SkipFunc: func(c core.Context) bool {
        return c.Path() == "/health"
    },
}))

// Route to slog instead
server.Use(middleware.Logger(middleware.LoggerConfig{
    LogFunc: func(c core.Context, status int, d time.Duration, err error) {
        slog.Info("request", "method", c.Method(), "path", c.FullURL(),
            "status", status, "ms", d.Milliseconds())
    },
}))

Production notes

  • The status is taken from the written response; for handler errors it uses the *core.HTTPError code, falling back to 500.
  • Errors are logged at Error level, successes at Info — useful for alerting on log level alone.

RequestID — Request Correlation IDs

middleware.RequestID() guarantees every request carries a unique ID: an incoming X-Request-ID header is reused, otherwise a random 16-byte hex ID is generated. The ID is stored in the context and echoed on the response header, so clients and log pipelines can correlate a single request end to end.

Defaults (DefaultRequestIDConfig()):

Field Type Default Description
Header string "X-Request-ID" Header to read the incoming ID from and write the outgoing ID to
Generator func() string random 16-byte hex (pooled) Creates a new ID when the header is absent
ContextKey string "request_id" Context key the ID is stored under
server.Use(middleware.RequestID())

// In a handler:
rid, _ := c.Get("request_id").(string)

Production notes

  • Register before Logger() so the request_id field appears in request logs.
  • Incoming IDs are trusted as-is; if your edge is untrusted, strip or validate the header at the load balancer.

Tracing — Distributed Tracing in Go

middleware.Tracing() propagates W3C Trace Context: it extracts the trace_id from an incoming traceparent header (or generates a 16-byte one), generates a fresh 8-byte span_id per request, stores both in the context, and echoes the trace ID back on X-Trace-ID for client-side correlation.

Defaults (DefaultTracingConfig()):

Field Type Default Description
TraceHeader string "traceparent" Incoming header parsed for the trace ID (W3C format)
TraceIDContextKey string "trace_id" Context key for the trace ID
SpanIDContextKey string "span_id" Context key for the span ID
ResponseHeader string "X-Trace-ID" Response header carrying the trace ID; "" disables
RecordDuration bool true Store handler duration (ms) under "trace_duration_ms"
server.Use(middleware.Tracing())

// In a handler or interceptor:
traceID, _ := c.Get("trace_id").(string)

Production notes

  • This middleware handles trace propagation only. For full OpenTelemetry (exporters, sampling, span hierarchies) use the OTel SDK’s middleware alongside it — the trace IDs will match since both read traceparent.
  • Register before Logger() so trace_id shows up in request logs.

Helmet — Security Headers Middleware

middleware.Helmet() sets OWASP-recommended security headers on every response, mirroring Node’s helmet for Go web apps. Set any string field to "" to disable that header.

Defaults (DefaultHelmetConfig()):

Field Header Default
ContentTypeNoSniff X-Content-Type-Options "nosniff"
XFrameOptions X-Frame-Options "SAMEORIGIN"
HSTS Strict-Transport-Security "max-age=63072000; includeSubDomains"
XXSSProtection X-XSS-Protection "0" (disabled — modern browsers use CSP)
ReferrerPolicy Referrer-Policy "strict-origin-when-cross-origin"
ContentSecurityPolicy Content-Security-Policy "" (not set — app-specific)
XDNSPrefetchControl X-DNS-Prefetch-Control "off"
CrossDomainPolicies X-Permitted-Cross-Domain-Policies "none"
XDownloadOptions X-Download-Options "noopen"
PermissionsPolicy Permissions-Policy "" (not set — app-specific)
server.Use(middleware.Helmet())

// Add a CSP and deny framing entirely
server.Use(middleware.Helmet(middleware.HelmetConfig{
    ContentTypeNoSniff:    "nosniff",
    XFrameOptions:         "DENY",
    HSTS:                  "max-age=63072000; includeSubDomains",
    ContentSecurityPolicy: "default-src 'self'",
    ReferrerPolicy:        "strict-origin-when-cross-origin",
}))

Production notes

  • Passing a custom config replaces the whole struct — fields you leave as "" are simply not sent, so restate the defaults you want to keep.
  • Only send HSTS on HTTPS deployments; browsers cache it aggressively.

CORS — Cross-Origin Resource Sharing in Go

middleware.CORS() handles CORS headers and preflight OPTIONS requests in Go. The default config is intentionally permissive for development (* origins); lock it down for production.

Defaults (DefaultCORSConfig()):

Field Type Default Description
AllowOrigins []string ["*"] Allowed origins; exact-match lookup, "*" allows all
AllowMethods []string GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD Access-Control-Allow-Methods
AllowHeaders []string Origin, Content-Type, Accept, Authorization, X-Request-ID Access-Control-Allow-Headers
ExposeHeaders []string nil Headers the browser may read (e.g. X-Total-Count)
AllowCredentials bool false Sends Access-Control-Allow-Credentials: true
MaxAge int 86400 Preflight cache time in seconds
// Production config
server.Use(middleware.CORS(middleware.CORSConfig{
    AllowOrigins:     []string{"https://app.example.com"},
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE"},
    AllowHeaders:     []string{"Content-Type", "Authorization"},
    ExposeHeaders:    []string{"X-Total-Count"},
    AllowCredentials: true,
    MaxAge:           3600,
}))

Production notes

  • Never ship AllowOrigins: ["*"] together with AllowCredentials: true — browsers reject that combination, and it defeats the origin check. List explicit origins when credentials are involved.
  • Preflight OPTIONS requests are answered directly with 204 No Content and never reach your handlers.
  • Header strings are pre-joined at startup, so the per-request cost is near zero.

RateLimit — Rate Limiting in Go

middleware.RateLimit() implements fixed-window rate limiting in Go with an in-memory, sharded store (32 shards, FNV-1a key hashing) to keep lock contention low under high concurrency. By default it limits each client IP to 100 requests per minute and sets X-RateLimit-* headers.

Defaults (DefaultRateLimitConfig()):

Field Type Default Description
Max int 100 Requests allowed per window
Window time.Duration time.Minute Fixed window length
KeyFunc func(c core.Context) string c.ClientIP() Key to count against (IP, user ID, API key…)
Message string "too many requests" Error message when limited
StatusCode int 429 Status when limited
SkipFunc func(c core.Context) bool nil Return true to bypass limiting
Headers bool true Emit X-RateLimit-Limit / -Remaining / -Reset
MaxKeys int 100000 Cap on distinct keys tracked in memory; when full, expired entries are evicted first, then the closest-to-expiry
Stop <-chan struct{} nil Close to stop the cleanup goroutine (tests, hot reload)
// 200 requests per 5 minutes, keyed by API key with IP fallback
server.Use(middleware.RateLimit(middleware.RateLimitConfig{
    Max:    200,
    Window: 5 * time.Minute,
    KeyFunc: func(c core.Context) string {
        if key := c.GetHeader("X-API-Key"); key != "" {
            return key
        }
        return c.ClientIP()
    },
}))

Production notes

  • Memory behavior: counters live in process memory. A background goroutine sweeps expired entries once per Window, and total tracked keys are hard-capped at MaxKeys (default 100,000) — when a shard fills, expired entries are evicted first, then the entry closest to expiry — so memory stays bounded even under unique-key floods (e.g. spoofed IPs). Pass Stop in tests to avoid leaking the goroutine.
  • Multi-replica: the store is per-process — with N replicas behind a load balancer, clients effectively get up to N× the limit. For a shared limit, enforce it at the edge (API gateway) or implement a Redis-backed limiter as custom middleware.
  • Fixed windows allow up to 2× Max in a burst straddling a window boundary; size Max with that in mind.
  • Behind a proxy, make sure your adapter is configured with trusted proxies (e.g. Gin’s SetTrustedProxies) so ClientIP() reflects the real client, not the load balancer — otherwise clients can rotate spoofed X-Forwarded-For values to dodge the limit. Never key on a raw X-Forwarded-For header value yourself.

Timeout — Request Timeout Middleware

middleware.Timeout() sets a deadline on the request’s context.Context via context.WithTimeout. Downstream I/O that respects the context — database queries, HTTP clients, gRPC — cancels automatically, and the client receives a 504.

Defaults (DefaultTimeoutConfig()):

Field Type Default Description
Timeout time.Duration 30 * time.Second Maximum handler duration
Message string "request timeout" Error message on expiry
StatusCode int 504 Status on expiry
SkipFunc func(c core.Context) bool nil Return true to skip (WebSocket, SSE)
// Global 30s default
server.Use(middleware.Timeout())

// Longer timeout for one route
r.POST("/reports", handler, middleware.Timeout(middleware.TimeoutConfig{
    Timeout: 2 * time.Minute,
}))

// Skip streaming endpoints
server.Use(middleware.Timeout(middleware.TimeoutConfig{
    SkipFunc: func(c core.Context) bool {
        return c.IsWebSocket() || c.GetHeader("Accept") == "text/event-stream"
    },
}))

Production notes

  • The deadline cancels context-aware work; it does not forcibly kill a handler goroutine that ignores its context (a CPU-bound loop, a blocking non-context-aware call). Pass the request context to every DB/HTTP/gRPC call (see core.RCtx() handler parameters) for the timeout to bite, and keep the server’s WriteTimeout set as the hard backstop.
  • The 504 is substituted only when the deadline fired and nothing has been written yet. An already-committed response is never overwritten, and a handler that merely finishes slightly late keeps its real response or error.

BodyLimit — Request Body Size Limit

middleware.BodyLimit() rejects requests whose body exceeds a maximum with 413 Payload Too Large. Enforcement is two-phase: a declared Content-Length above the limit is rejected before the body is read, and the actual body bytes are then measured — so chunked requests and lying Content-Length headers cannot bypass the limit. Use it per route to override the global Config.BodyLimit.

Config (BodyLimitConfig — no defaults constructor; fallbacks applied when zero):

Field Type Default Description
MaxBytes int64 4 * 1024 * 1024 (4MB) Maximum allowed body size in bytes
Message string "request body too large" Error message on rejection
// Allow 100MB on the upload route only
r.POST("/upload", handler, middleware.BodyLimit(middleware.BodyLimitConfig{
    MaxBytes: 100 * 1024 * 1024,
}))

// Tight limit for small JSON endpoints
r.POST("/api/data", handler, middleware.BodyLimit(middleware.BodyLimitConfig{
    MaxBytes: 1024,
}))

Production notes

  • The Content-Length fast path costs nothing for compliant clients; chunked requests (no Content-Length) are caught by the actual-bytes check. Adapters cache the body, so handlers can still call c.Body()/c.Bind() normally afterwards. The global Config.BodyLimit remains the transport-level backstop, enforced by both adapters.
  • The rejected response includes an X-Body-Limit header with the configured maximum.

CSRF — CSRF Protection in Go

middleware.CSRF() implements the double-submit cookie pattern for Cross-Site Request Forgery protection in Go: a random token is set as a cookie, and on unsafe methods (POST, PUT, PATCH, DELETE) the client must echo the same token in a header or form field. Safe methods (GET, HEAD, OPTIONS, TRACE) always pass. Comparison is constant-time to prevent timing attacks.

Defaults (DefaultCSRFConfig()):

Field Type Default Description
TokenLength int 32 Token bytes (hex-encoded to 64 chars)
CookieName string "_csrf" Cookie storing the token
HeaderName string "X-CSRF-Token" Header the client sends the token in
FormField string "_csrf" Form-field fallback when the header is missing
CookiePath string "/" Cookie path
CookieDomain string "" Cookie domain (current domain)
CookieSecure bool false Secure flag — set true in production
SameSite string "Lax" Cookie SameSite attribute ("Lax", "Strict", "None"); applied when the adapter implements core.SameSiteCookieSetter (both official adapters do)
CookieHTTPOnly bool true HttpOnly flag
CookieMaxAge int 86400 Cookie lifetime in seconds (24h)
SkipFunc func(c core.Context) bool nil Return true to skip validation
ErrorHandler func(c core.Context) error 403 "CSRF token mismatch" Called on validation failure
TokenGenerator func() (string, error) crypto/rand hex Custom token source
// Cookie-session apps: protect everything, skip the JWT API
server.Use(middleware.CSRF(middleware.CSRFConfig{
    CookieSecure: true,
    SkipFunc: func(c core.Context) bool {
        return strings.HasPrefix(c.Path(), "/api/")
    },
}))

// In a template handler, expose the token to the page:
token, _ := c.Get("csrf_token").(string)

Production notes

  • Always set CookieSecure: true in production — the double-submit defense depends on attackers being unable to plant the cookie, and a non-Secure cookie can be set by a plaintext-HTTP MITM. The cookie is written with SameSite=Lax by default (on adapters without core.SameSiteCookieSetter, it is set without a SameSite attribute). For SPA frameworks that read the cookie via JavaScript (Angular-style XSRF-TOKEN), set CookieHTTPOnly: false and rename CookieName/HeaderName accordingly — the double-submit pattern stays sound because cross-site pages cannot read your cookies.
  • CSRF matters for cookie/session authentication. Pure bearer-token APIs (JWT in the Authorization header) are not CSRF-able — skip them via SkipFunc rather than weakening the config.
  • The current token is always available to handlers under the "csrf_token" context key for rendering into forms.

Compress — Gzip Compression in Go

middleware.Compress() enables gzip compression in Go for JSON responses. It checks the client’s Accept-Encoding and stashes the compression settings in the context; handlers opt in by responding with the middleware.GzipJSON(c, status, data) helper instead of c.JSON(...). Responses smaller than MinLength are sent uncompressed.

Defaults (DefaultCompressConfig()):

Field Type Default Description
Level int gzip.DefaultCompression Gzip level 1–9 (gzip.BestSpeed … gzip.BestCompression)
MinLength int 1024 Minimum body size in bytes to compress
SkipFunc func(c core.Context) bool nil Return true to skip
server.Use(middleware.Compress())

// Handler opts in with GzipJSON
func (ctrl *ReportController) List(c core.Context) error {
    data := ctrl.service.GetLargeDataset()
    return middleware.GzipJSON(c, 200, data)
}

Production notes

  • GzipJSON degrades gracefully: without the middleware (or a client that doesn’t accept gzip) it behaves exactly like c.JSON. Compressed responses carry Content-Encoding: gzip and Vary: Accept-Encoding.
  • For transparent compression of every response type, use the adapter-native option instead: github.com/gin-contrib/gzip on Gin, or Fiber’s built-in compress middleware.
  • Don’t compress tiny payloads — the default 1KB threshold avoids paying gzip overhead for bodies that fit in one packet anyway.
  • GzipJSON pools gzip writers per compression level (sync.Pool), reusing the flate compressor’s internal state across responses instead of reallocating it per call.

ETag — HTTP Caching with ETags

middleware.ETag() computes an ETag from the response body (SHA-256, truncated to 32 hex chars) on GET/HEAD requests and answers matching If-None-Match requests with 304 Not Modified and no body — a large bandwidth win on read-heavy Go APIs. Serving the 304 requires the adapter to discard the buffered response first (core.ResponseResetter); both official adapters implement it.

Defaults (DefaultETagConfig()):

Field Type Default Description
Weak bool true Emit weak ETags (W/"...") — safest for JSON APIs
SkipFunc func(c core.Context) bool nil Return true to skip (e.g. streaming)
server.Use(middleware.ETag())

// Strong ETags when responses are byte-exact
server.Use(middleware.ETag(middleware.ETagConfig{Weak: false}))

// Skip SSE endpoints
server.Use(middleware.ETag(middleware.ETagConfig{
    SkipFunc: func(c core.Context) bool {
        return c.GetHeader("Accept") == "text/event-stream"
    },
}))

Production notes

  • ETags are only generated for successful (2xx) GET/HEAD responses with a non-empty body; errors and mutations are untouched.
  • The 304 rewrite happens only when the adapter context implements core.ResponseResetter, so the buffered 200 can be discarded before it reaches the wire. The Gin and Fiber adapters both do; on a third-party adapter without it, only the ETag header is set and the full response is sent — never a status rewrite on top of a committed body.
  • The handler still runs on a 304 — the saving is response bandwidth, not compute. For compute savings, cache at the service layer.
  • If responses are gzip-compressed by an adapter-level compressor after this middleware, prefer weak ETags (the default), since the bytes on the wire differ from the hashed body.

Idempotency — Idempotency Middleware for Safe Retries

middleware.Idempotency() is an idempotency middleware that caches successful responses by a client-supplied Idempotency-Key header. A retry with the same key within the TTL returns the cached response immediately — with an Idempotency-Replayed: true header and the original Content-Type — without re-running the handler. Cache keys are hashed (SHA-256) from the method, path, an optional KeyScopeFunc scope, and the client key, so a key reused against a different endpoint — or, with scoping, by a different user — can never replay the wrong response. Concurrent duplicates get 409 Conflict while the first request is in flight, and failed requests (errors, non-2xx statuses, even panics) release the key so retries can succeed. This is essential for payment APIs and any non-idempotent mutation.

Defaults (DefaultIdempotencyConfig()):

Field Type Default Description
Header string "Idempotency-Key" Header carrying the client’s unique key
TTL time.Duration 24 * time.Hour How long cached responses are kept
Methods []string POST, PUT, PATCH Methods idempotency applies to
Required bool false Return 400 when the header is missing
KeyScopeFunc func(c core.Context) string nil Extra scope hashed into the cache key (e.g. user/tenant ID) — strongly recommended on multi-tenant APIs
MaxEntries int 10000 Cap on entries in the default in-memory store; ignored when a custom Store is provided
Store IdempotencyStore in-memory store Pluggable backing store
Stop <-chan struct{} nil Close to stop the in-memory store’s cleanup goroutine
// Require an idempotency key on the payments route, scoped per user
r.POST("/payments", handler, middleware.Idempotency(middleware.IdempotencyConfig{
    Required: true,
    TTL:      1 * time.Hour,
    KeyScopeFunc: func(c core.Context) string {
        userID, _ := c.Get("user_id").(string)
        return userID // one user's key can never replay another user's response
    },
}))

Production notes

  • Multi-replica deployments need a shared store. The default store is in-process memory — with several replicas, a retry may land on a replica that never saw the key and re-execute the handler. Implement the IdempotencyStore interface over Redis and pass it via Store:
// IdempotencyStore — implement over Redis for multi-replica safety
type IdempotencyStore interface {
    Get(key string) *IdempotencyEntry
    Set(key string, entry *IdempotencyEntry, ttl time.Duration)
    SetProcessing(key string, ttl time.Duration) bool // false if already in flight
    Remove(key string)
}

server.Use(middleware.Idempotency(middleware.IdempotencyConfig{
    Store: myRedisIdempotencyStore,
}))
  • In a Redis implementation, back SetProcessing with SET key ... NX PX ttl so the in-flight lock is atomic across replicas.
  • Only 2xx responses are cached (IdempotencyEntry{Status, Body, ContentType} — the original Content-Type is stored and replayed). Handler errors and non-2xx statuses call Store.Remove so the client can retry, and a panicking handler releases the key before re-panicking instead of blocking retries with 409s until the TTL expires.
  • The default in-memory store is sharded 32 ways (FNV-1a, same scheme as the rate limiter), bounded by MaxEntries (expired entries evicted first, then the oldest-expiring), and sweeps expired entries every min(TTL/2, 1 minute).
  • The client key is never used alone — the store key is SHA-256(method + path + scope + key). Clients should still send a UUID per logical operation, not per HTTP attempt.

Upload — File Upload Validation

middleware.Upload() validates a multipart file upload before your handler runs: size limit, allowed extensions, and MIME type sniffed from the first 512 bytes of file content (not the spoofable client header). On success it stores an *UploadedFile in the context under "uploaded_file".

Defaults (DefaultUploadConfig()):

Field Type Default Description
FieldName string "file" Multipart form field containing the file
MaxFileSize int64 10 * 1024 * 1024 (10MB) Max size; larger files get 413
AllowedMIMETypes []string nil (all allowed) e.g. ["image/png", "image/jpeg", "application/pdf"]
AllowedExtensions []string nil (all allowed) With dot, e.g. [".png", ".jpg", ".pdf"]
r.POST("/avatar", handler, middleware.Upload(middleware.UploadConfig{
    MaxFileSize:      5 * 1024 * 1024,
    AllowedMIMETypes: []string{"image/png", "image/jpeg"},
}))

func (ctrl *ProfileController) UploadAvatar(c core.Context) error {
    uf := c.Get("uploaded_file").(*middleware.UploadedFile)
    if err := middleware.SaveFile(uf.Header, "/uploads/"+uf.Filename); err != nil {
        return core.ErrInternalServer("could not save file")
    }
    return c.JSON(201, map[string]any{"size": uf.Size, "type": uf.MIMEType})
}

Production notes

  • Pair with BodyLimit (or the global Config.BodyLimit) sized above MaxFileSize — MaxFileSize checks the parsed file, while the body limit rejects oversized requests before parsing.
  • MIME detection uses http.DetectContentType on real file bytes, so a renamed .exe cannot masquerade as a PNG. Combine MIME and extension checks for defense in depth.
  • Never write uploads using the client-supplied filename verbatim — generate your own name (the example above is simplified). middleware.SaveFile creates missing destination directories for you.

Next steps

  • Guards — authentication and authorization checks that run before handlers
  • Interceptors — wrap handler execution for logging, caching, and response mapping
  • Validation & Error Handling — HTTPError, exception filters, and how middleware errors become responses

Browse the middleware source on GitHub: github.com/ashrafAli23/nestgo.