Middleware

Introduction

Middleware are components that sit between the incoming HTTP request and your controller logic. They allow you to execute code before and/or after a request is handled. This is particularly useful for tasks such as authentication, logging, request modification, and response transformation. In Confetti, middleware helps keep your controller code clean by handling cross-cutting concerns in a centralized manner.

Creating a Middleware

A middleware in Confetti is typically implemented as a type that provides a Handle method. This method wraps your controller function, allowing you to execute custom logic before and/or after the controller is called.

Basic Middleware Example

Below is an example of a simple logging middleware that logs the request method and URL before passing control to the next handler:

package middleware

import (
	"log"
	"net/http"
	"src/internal/pkg/handler"
)

// LoggingMiddleware is a simple middleware that logs requests.
type LoggingMiddleware struct{}

// Handle wraps a controller with logging functionality.
func (l LoggingMiddleware) Handle(next handler.Controller) handler.Controller {
	return func(w http.ResponseWriter, req *http.Request) error {
		// Before: Log the incoming request
		log.Printf("Incoming request: %s %s", req.Method, req.URL.Path)
		
		// Call the next handler in the chain
		err := next(w, req)
		
		// After: Optionally log the outcome (if needed)
		if err != nil {
			log.Printf("Error processing request: %v", err)
		}
		
		return err
	}
}

Registering Middleware

Once you’ve created your middleware, you need to register it with your routes. In Confetti, routes are defined with the helper handler.New function. You can append middleware to a route using a method like AppendMiddleware.

Registering a Middleware for a Route

Below is an example of how to register the LoggingMiddleware with a specific route:

package routes

import (
	"src/internal/pkg/handler"
	"src/internal/controllers"
	"src/internal/middleware"
)

var ApiRoutes = []handler.Route{
	handler.New("GET /api/example", controllers.ExampleController).
		AppendMiddleware(middleware.LoggingMiddleware{}),
}

In this example, every time the /api/example endpoint is hit, the LoggingMiddleware will execute before the ExampleController is called.

Handy Middleware Examples

1. Authentication Middleware

This middleware checks whether the request contains the proper authentication token or permission before allowing it to proceed.

package middleware

import (
	"net/http"
	"src/internal/pkg/handler"
)

// AuthMiddleware checks for the required permission.
type AuthMiddleware struct {
	Permission string
}

// Handle wraps a controller with authentication logic.
func (a AuthMiddleware) Handle(next handler.Controller) handler.Controller {
	return func(w http.ResponseWriter, req *http.Request) error {
		// Perform authentication check (this is just a placeholder).
		if req.Header.Get("Authorization") == "" {
			return handler.NewUserError("Unauthorized: missing token", http.StatusUnauthorized)
		}
		// Optionally check for specific permissions.
		// if !hasPermission(req, a.Permission) { ... }

		// Proceed to the next handler if authenticated.
		return next(w, req)
	}
}

2. Request Validation Middleware

This middleware validates the incoming request data before it reaches the controller.

package middleware

import (
	"encoding/json"
	"net/http"
	"src/internal/pkg/handler"
)

// ValidateMiddleware ensures that the request body conforms to expected structure.
type ValidateMiddleware struct{}

// Handle parses and validates the request body.
func (v ValidateMiddleware) Handle(next handler.Controller) handler.Controller {
	return func(w http.ResponseWriter, req *http.Request) error {
		var data map[string]any
		decoder := json.NewDecoder(req.Body)
		if err := decoder.Decode(&data); err != nil {
			return handler.NewUserError("Invalid request payload", http.StatusBadRequest)
		}

		// You could attach validated data to the request context here.
		// For simplicity, we proceed to the next handler.
		return next(w, req)
	}
}

Conclusion

Middleware in Confetti offers a powerful and flexible way to handle common tasks such as logging, authentication, and validation without cluttering your controller logic. By creating reusable middleware components and registering them with your routes, you can ensure a clean separation of concerns, maintainable code, and a consistent behavior across your application.

Contributors: reindert-vetter, Reindert Vetter, Vaggelis Yfantis