BackendGoAPIBackend

Building Scalable APIs with Go Fiber

A deep dive into building production-grade REST APIs with Go Fiber, including middleware patterns, error handling, and database integration.

Maulik Joshi

Full-Stack Developer

January 28, 2026
8 min read

Go has become one of the most popular languages for building backend services, and for good reason. Its simplicity, performance, and built-in concurrency primitives make it an excellent choice for production APIs. In this post, we'll explore how to build scalable REST APIs using Go Fiber — a fast, Express-inspired framework.

Why Go Fiber?

Fiber is built on top of Fasthttp, the fastest HTTP engine for Go. It's designed to ease the development process with features similar to Express.js, making it especially approachable if you're coming from a Node.js background.

  • Up to 10x faster than net/http for certain workloads
  • Express-style routing and middleware API
  • Built-in rate limiting, CORS, and compression
  • Zero memory allocation in hot paths
  • Extensive middleware ecosystem

Project Setup

Let's start by initializing a new Go module and installing Fiber:

terminal
bash
1mkdir fiber-api && cd fiber-api
2go mod init github.com/yourusername/fiber-api
3go get github.com/gofiber/fiber/v2
4go get github.com/gofiber/fiber/v2/middleware/cors
5go get github.com/gofiber/fiber/v2/middleware/logger
6go get github.com/gofiber/fiber/v2/middleware/recover

Application Structure

A well-organized project structure is crucial for maintainability. Here's the layout we'll follow:

project structure
text
1fiber-api/
2├── cmd/
3│ └── server/
4│ └── main.go # Entry point
5├── internal/
6│ ├── config/
7│ │ └── config.go # Environment configuration
8│ ├── database/
9│ │ └── postgres.go # Database connection
10│ ├── handlers/
11│ │ ├── user.go # User handlers
12│ │ └── health.go # Health check
13│ ├── middleware/
14│ │ ├── auth.go # JWT authentication
15│ │ └── ratelimit.go # Rate limiting
16│ ├── models/
17│ │ └── user.go # Data models
18│ └── routes/
19│ └── routes.go # Route definitions
20├── go.mod
21└── go.sum

Core Server Setup

Here's our main entry point with essential middleware configured:

cmd/server/main.go
go
1package main
2
3import (
4 "log"
5 "os"
6
7 "github.com/gofiber/fiber/v2"
8 "github.com/gofiber/fiber/v2/middleware/cors"
9 "github.com/gofiber/fiber/v2/middleware/logger"
10 "github.com/gofiber/fiber/v2/middleware/recover"
11)
12
13func main() {
14 app := fiber.New(fiber.Config{
15 AppName: "Fiber API v1.0",
16 ServerHeader: "Fiber",
17 ErrorHandler: customErrorHandler,
18 })
19
20 // Global middleware
21 app.Use(recover.New())
22 app.Use(logger.New(logger.Config{
23 Format: "${time} | ${status} | ${latency} | ${method} ${path}\n",
24 }))
25 app.Use(cors.New(cors.Config{
26 AllowOrigins: "http://localhost:3000",
27 AllowHeaders: "Origin, Content-Type, Accept, Authorization",
28 }))
29
30 // Health check
31 app.Get("/health", func(c *fiber.Ctx) error {
32 return c.JSON(fiber.Map{
33 "status": "healthy",
34 "version": "1.0.0",
35 })
36 })
37
38 // API routes
39 api := app.Group("/api/v1")
40 setupRoutes(api)
41
42 port := os.Getenv("PORT")
43 if port == "" {
44 port = "8080"
45 }
46
47 log.Fatal(app.Listen(":" + port))
48}

Custom Error Handling

Tip

A consistent error response format is essential for API consumers. Always return structured error objects with a code, message, and optional details field.

One of the most important patterns in production APIs is centralized error handling. Here's a custom error handler that provides consistent JSON responses:

internal/handlers/errors.go
go
1type APIError struct {
2 Code int `json:"code"`
3 Message string `json:"message"`
4 Details string `json:"details,omitempty"`
5}
6
7func customErrorHandler(c *fiber.Ctx, err error) error {
8 code := fiber.StatusInternalServerError
9 message := "Internal Server Error"
10
11 // Check for Fiber-specific errors
12 if e, ok := err.(*fiber.Error); ok {
13 code = e.Code
14 message = e.Message
15 }
16
17 return c.Status(code).JSON(APIError{
18 Code: code,
19 Message: message,
20 })
21}

Middleware Patterns

Middleware is where Fiber truly shines. Let's implement JWT authentication middleware that validates tokens and injects user context:

internal/middleware/auth.go
go
1func AuthRequired() fiber.Handler {
2 return func(c *fiber.Ctx) error {
3 token := c.Get("Authorization")
4 if token == "" {
5 return fiber.NewError(401, "Missing authorization token")
6 }
7
8 // Strip "Bearer " prefix
9 if len(token) > 7 && token[:7] == "Bearer " {
10 token = token[7:]
11 }
12
13 claims, err := validateJWT(token)
14 if err != nil {
15 return fiber.NewError(401, "Invalid or expired token")
16 }
17
18 // Inject user into context
19 c.Locals("userID", claims.UserID)
20 c.Locals("role", claims.Role)
21
22 return c.Next()
23 }
24}

Database Integration

For production APIs, you'll want a robust database layer. Here's how to set up PostgreSQL with connection pooling:

internal/database/postgres.go
go
1package database
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "github.com/jackc/pgx/v5/pgxpool"
9)
10
11var Pool *pgxpool.Pool
12
13func Connect(databaseURL string) error {
14 config, err := pgxpool.ParseConfig(databaseURL)
15 if err != nil {
16 return fmt.Errorf("parse config: %w", err)
17 }
18
19 config.MaxConns = 25
20 config.MinConns = 5
21 config.MaxConnLifetime = 5 * time.Minute
22 config.MaxConnIdleTime = 1 * time.Minute
23
24 Pool, err = pgxpool.NewWithConfig(context.Background(), config)
25 if err != nil {
26 return fmt.Errorf("create pool: %w", err)
27 }
28
29 // Verify connection
30 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
31 defer cancel()
32
33 return Pool.Ping(ctx)
34}
Warning

Always set MaxConns based on your database plan limits. For Neon free tier, keep it under 25. For production, tune based on your expected concurrency.

Performance Tips

  1. Use Fiber's built-in JSON serializer — it's optimized for zero-allocation encoding
  2. Enable Prefork mode in production for multi-core utilization
  3. Implement request validation at the handler level with struct tags
  4. Use connection pooling for all external services (DB, Redis, HTTP clients)
  5. Add response caching for read-heavy endpoints with Fiber's Cache middleware

Wrapping Up

Go Fiber provides an excellent foundation for building high-performance APIs. Its familiar Express-like API makes the transition from Node.js smooth, while Go's type system and performance characteristics give you a rock-solid production backend. The patterns we've covered — structured error handling, middleware chains, and proper database pooling — will serve you well as your API scales.

The best API is one that's boring to operate. Focus on reliability, consistent error responses, and clear documentation — your future self and your team will thank you.

Newsletter

Stay updated with new posts

Get notified when I publish new articles. No spam, unsubscribe anytime.