Skip to main content

Building a High-Performance API Gateway

How LZStock bridges frontend RESTful clients with internal gRPC microservices using a zero-allocation Fasthttp API Gateway.

TL;DR
  • REST-to-gRPC Protocol Translation: Acted as an Anti-Corruption Layer (ACL) translating frontend JSON to internal Protobuf, shielding clients from internal microservice complexities.
  • Zero-Allocation High Performance: Swapped standard net/http for Fasthttp to process massive concurrent traffic with virtually zero memory bloat and garbage collection overhead.
  • Kill Zombie Requests & Pod Crashes: Implemented strict context propagation to instantly cancel internal gRPC processes if a client drops, alongside global panic recovery to keep the gateway resilient.

The Objective

Frontend applications (React/Next.js) speak HTTP/JSON natively, but our internal microservices communicate via highly optimized, binary gRPC/Protobuf.

Exposing gRPC directly to the web (via gRPC-Web) tightly couples the client to our internal domain models. The objective of this API Gateway is to act as an Anti-Corruption Layer (ACL) and Protocol Translator. It ingests millions of standard RESTful HTTP requests, enforces authentication, translates the JSON payloads into Protobuf, and routes them to the correct internal microservice—all with virtually zero memory allocation overhead.

The Mental Model & Request Lifecycle

The Gateway strictly prohibits business logic. Its sole purpose is cross-cutting concerns (Middleware) and translation.

Codebase Anatomy

mods/bc15-api-gateway
├── persistence
└── server
├── middle
│ ├── auth.go
│ ├── cors.go
│ └── error.go
└── restful
├── bc1-indicator-insights
├── bc11-market-monitor
├── bc13-investor
├── bc3-company-selector
└── v1

Core Implementation

To handle massive concurrency, I bypassed the standard net/http library in favor of valyala/fasthttp, which is optimized for zero memory allocation in hot paths.

The Global Router & Middleware Pipeline

The router establishes the API versioning strategy and injects global fail-safes. If a downstream translation panics, the PanicError middleware catches it, preventing the entire gateway pod from crashing.

// mods/bc15-api-gateway/server/restful/v1.go

func V1Router(router *routing.Router) *routing.Router {
// 1. Global Cross-Cutting Concerns
router.Use(middleware.CORS)
router.Use(middleware.PanicError)

// 2. Strict API Versioning Boundary
api := router.Group("/" + VERSION) // e.g., /v1

// 3. Mount Domain Sub-Routers
bc13InvestorRouter.InvestorRouter(api)

router.NotFound(func(c *routing.Context) error {
restful.NotFound(c, "Route not found")
return nil
})

return router
}

The Protocol Translator (REST ➡️ gRPC)

This handler demonstrates the core responsibility of the gateway. Notice the strict lifecycle: Timeout Context Injection ➡️ Payload Mapping ➡️ gRPC Invocation ➡️ Error Translation.

// mods/bc15-api-gateway/server/restful/bc13-investor

func InvestorRouter(api *routing.RouteGroup) {
// Protected by Auth Middleware
api.Get("/investor", middleware.Authenticate, func(c *routing.Context) (err error) {
// 1. Fail-safe recovery per handler
defer apperrs.Recover(&err)

// 2. Context Hydration & Timeout Propagation
// Ties the gRPC context to the HTTP request lifecycle.
ctx := LZContext.New(c).WithTimeout(time.Second * 5)

// 3. Input Validation
investorID := string(c.RequestCtx.QueryArgs().Peek("investor_id"))
if investorID == "" {
restful.Fail(c, apperrs.ErrEmptyInvestorID)
return nil
}

// 4. Protocol Translation: REST -> Protobuf
req := &pb.GetInvestorReq{InvestorId: investorID}

// 5. Invoke Internal Microservice
res, err := gRPC.Services().InvestorServices.GetInvestor(ctx, req)
if err != nil {
// 6. Error Translation: Maps gRPC status codes (e.g., codes.NotFound)
// to standard HTTP 4xx/5xx status codes for the frontend.
restful.HandleGRPCErr(c, err, apperrs.GetInvestorErrMessageMap)
return nil
}

// 7. Successful Response Translation
restful.OK(c, res)
return nil
})
}

Edge Cases & Trade-offs

  • fasthttp vs. Standard net/http: I chose fasthttp for its raw performance and zero-allocation capabilities, which are crucial for an API Gateway processing every single inbound request. However, this comes with a trade-off: fasthttp does not fully support HTTP/2.0 natively like the standard library does, and its memory lifecycle (reusing byte slices) requires extreme caution to avoid data races.
  • Context Cancellation (Zombie Requests): If a mobile user has a network drop and abandons their HTTP request, we do not want the internal microservices to continue crunching data. By wrapping the fasthttp context into a standard context.Context (LZContext.New(c)), if the HTTP connection drops, the propagated gRPC context is immediately canceled, killing the internal process and saving CPU cycles.

The Outcome

By leveraging a zero-allocation router and enforcing strict context propagation, the API Gateway safely multiplexes frontend JSON traffic into high-throughput gRPC streams, while providing a centralized shield for authentication and error translation.