08/05/2026 06:52am

JS2GO EP.46 Building Middleware and Modular Architecture in Go and Node.js
#Node.js
#Modular Architecture
#Go
#Middleware
Write APIs that simply “run” = easy.
Write APIs that scale, are maintainable, and survive Production = requires architecture.
This episode takes you from fundamentals → enterprise-level architecture.
By the end of this chapter, you will understand:
✔ How to build Middleware (Auth, Logging, Rate Limit)
✔ The Service / Repository Pattern
✔ Modular Architecture that is Production-ready
✔ Correct examples using Express (Node.js) & Fiber (Go)
✔ Best Practices used in real-world companies
⭐ 1) What Is Middleware?
Middleware is a function that executes before (or after) the main handler.
Common responsibilities include:
- Authorization / Token verification
- Request logging
- Rate limiting
- Request transformation (e.g., normalize headers)
- Validation
- Permission checks
Core execution flow (same in Go and Node.js):
Request → Middleware A → Middleware B → Handler → Response
Handlers should remain thin, delegating work to Middleware or the Service Layer.
⭐ 2) Real-world Middleware Examples (Correct and Production-safe)
🔹 Logging Middleware
Go (Fiber)
func Logging() fiber.Handler {
return func(c *fiber.Ctx) error {
start := time.Now()
err := c.Next()
fmt.Printf("[%s] %s - %d (%v)\n",
c.Method(),
c.Path(),
c.Response().StatusCode(),
time.Since(start),
)
return err
}
}
Register:
app.Use(Logging())
Node.js (Express)
function logger(req, res, next) {
const start = Date.now();
res.on("finish", () => {
console.log(`[${req.method}] ${req.url} - ${res.statusCode} (${Date.now() - start}ms)`);
});
next();
}
app.use(logger);
🔹 Authentication Middleware
Go (Fiber)
func Auth() fiber.Handler {
return func(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
return c.Status(401).JSON(fiber.Map{
"error": "Missing Authorization header",
})
}
return c.Next()
}
}
Node.js (Express)
function auth(req, res, next) {
const token = req.headers.authorization;
if (!token) return res.status(401).json({ error: "Unauthorized" });
next();
}
app.use(auth);
🔹 Basic Rate Limit (for demonstration only)
Go
func RateLimit() fiber.Handler {
var mu sync.Mutex
var count = 0
return func(c *fiber.Ctx) error {
mu.Lock()
if count >= 100 {
mu.Unlock()
return c.Status(429).SendString("Too Many Requests")
}
count++
mu.Unlock()
return c.Next()
}
}
Node.js
let count = 0;
function rateLimit(req, res, next) {
if (count >= 100) {
return res.status(429).send("Too Many Requests");
}
count++;
next();
}
app.use(rateLimit);
➡ Production Note: Use Redis + Token Bucket / Sliding Window for distributed rate limits.
⭐ 3) What Is Modular Architecture?
A modular system is structured so each module is self-contained, reducing coupling and increasing maintainability.
Benefits:
✔ Easy to modify
✔ Easy to add features without touching other modules
✔ Easy to test
✔ Works for medium to large systems
📌 Production-ready Go Structure
/cmd/server/main.go
/internal
/user
handler.go
service.go
repository.go
model.go
/product
handler.go
service.go
repository.go
/pkg
/database
📌 Production-ready Node.js Structure
src/
├─ modules/
│ ├─ user/
│ │ ├─ user.controller.js
│ │ ├─ user.service.js
│ │ ├─ user.repository.js
│ │ ├─ user.model.js
│ ├─ product/
├─ middlewares/
├─ routes/
├─ database/
├─ app.js
├─ server.js
⭐ 4) Service / Repository Pattern Explained
Separates responsibilities across layers:
| Layer | Responsibility |
|---|---|
| Controller / Handler | Receive request → call service → return response |
| Service | Business logic, validation, workflow |
| Repository | Database operations only |
Example in Go (Fiber)
Handler
func (h *UserHandler) GetUser(c *fiber.Ctx) error {
id := c.Params("id")
user, err := h.Service.GetUser(c.Context(), id)
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "User not found"})
}
return c.JSON(user)
}
Service
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
return s.Repo.FindByID(ctx, id)
}
Repository
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
row := r.DB.QueryRow(ctx, "SELECT id, name FROM users WHERE id=$1", id)
var u User
if err := row.Scan(&u.ID, &u.Name); err != nil {
return nil, err
}
return &u, nil
}
Example in Node.js (Express)
Controller
export const getUser = async (req, res) => {
const user = await userService.getUser(req.params.id);
if (!user) return res.status(404).send("User not found");
res.json(user);
};
Service
export const userService = {
getUser: async (id) => userRepository.findById(id),
};
Repository
export const userRepository = {
findById: async (id) => {
const result = await pool.query(
"SELECT id, name FROM users WHERE id=$1",
[id]
);
return result.rows[0];
},
};
⭐ 5) Production Best Practices
✔ Controllers must stay thin
✔ Services must not query the database directly
✔ Repositories must not embed business logic
✔ Keep middleware lightweight (avoid heavy computation)
✔ Use Dependency Injection (covered in EP.47)
✔ Name files by responsibility (e.g., user.service.js)
✔ Mock repositories/services for testing
📌 Summary
When you combine:
- Well-designed Middleware
- Modular Architecture
- Service/Repository Pattern
You get systems that are:
🚀 Scalable
🧱 Stable
🔍 Testable
💼 Production-ready
This is the architecture used by real companies and enterprise-grade backend teams.
🔵 Next Episode: EP.47 Dependency Injection in Go & Node.js
You will learn:
✔ Why DI makes systems testable
✔ Constructor-based DI in Go
✔ DI Container in Node.js
✔ How to mock services & repositories
✔ Designing a correct Dependency Graph