Docs Thin Handlers

Thin Handlers

“Keep handlers thin” is a common recommendation.

But without a concrete definition, it quickly becomes meaningless.

In Plumego, thin handlers are not a style preference
they are a structural requirement that protects boundaries, testability, and long-term maintainability.

This document defines what “thin” actually means,
and how to enforce it in real systems.


What a Handler Is — and Is Not

A handler is the HTTP boundary.

It exists to translate between:

  • HTTP-level concepts (request, response)
  • Application-level concepts (use cases, inputs, outputs)

A handler is not:

  • A place for business rules
  • A workflow engine
  • A persistence layer
  • A dumping ground for logic

If handlers grow large, boundaries are already eroding.


The Three Responsibilities of a Thin Handler

A thin handler does exactly three things:

  1. Extract input
    Read parameters, headers, and request bodies.

  2. Invoke application logic
    Call a usecase or application service.

  3. Translate output
    Convert results and errors into HTTP responses.

If a handler does anything else, it is no longer thin.


A Minimal Thin Handler Example

func CreateOrderHandler(ctx *plumego.Context) {
	var req CreateOrderRequest
	if err := ctx.BindJSON(&req); err != nil {
		ctx.JSON(http.StatusBadRequest, errorResponse(err))
		return
	}

	id, err := createOrderUsecase.Execute(req.ToInput())
	if err != nil {
		handleError(ctx, err)
		return
	}

	ctx.JSON(http.StatusCreated, map[string]string{
		"id": id,
	})
}

Notice what is not present:

  • No business rules
  • No persistence logic
  • No conditional workflows
  • No framework leakage inward

What Makes a Handler “Fat”

Handlers become fat when they start answering questions they should not.

Common fat-handler smells

  • Long functions (50+ lines)
  • Nested if/else trees
  • Business terminology mixed with HTTP details
  • Database calls
  • Conditional logic based on domain rules
  • Duplicate logic across handlers

These are structural smells, not stylistic ones.


Where Logic Should Go Instead

Logic TypeProper Location
HTTP parsingHandler
AuthenticationMiddleware
Authorization rulesHandler / Usecase
Workflow orchestrationUsecase
Business rulesDomain
PersistenceInfrastructure
Validation (business)Domain / Usecase

Handlers should remain boring.


Validation: Boundary vs Business

Not all validation is equal.

Boundary validation (handler-level)

  • Required fields
  • Basic type checks
  • Malformed payloads

Business validation (domain-level)

  • Invariants
  • State transitions
  • Rule enforcement

Mixing the two leads to duplicated logic and inconsistencies.


Error Handling Without Fat Handlers

Handlers should not implement complex error trees.

Instead:

  • Let usecases return domain/application errors
  • Centralize HTTP error translation
  • Keep handler error paths shallow

Example:

func handleError(ctx *plumego.Context, err error) {
	switch {
	case errors.Is(err, ErrForbidden):
		ctx.JSON(http.StatusForbidden, errorResponse(err))
	case errors.Is(err, ErrNotFound):
		ctx.JSON(http.StatusNotFound, errorResponse(err))
	default:
		ctx.JSON(http.StatusInternalServerError, errorResponse("internal error"))
	}
}

This keeps handlers readable and consistent.


Thin Handlers and Testing

Thin handlers are easy to test because:

  • Inputs are explicit
  • Outputs are deterministic
  • Business logic is mocked or stubbed
  • No hidden dependencies exist

Most bugs surface in usecases and domain logic —
exactly where they should.


Thin Handlers Enable Transport Flexibility

When handlers are thin:

  • You can add new transports (RPC, CLI, workers)
  • Reuse application logic
  • Change HTTP frameworks if needed

Fat handlers lock your system to a single delivery mechanism.


Enforcing Thin Handlers in Practice

Thin handlers are a discipline, not an accident.

Effective enforcement techniques:

  • Code review rules (handler size, imports)
  • Package boundaries (internal/http)
  • Naming conventions (*Handler vs *Service)
  • Regular refactoring

Treat handler bloat as architectural debt.


When a Handler Is Allowed to Grow

Occasionally, handlers may grow temporarily during:

  • Early prototyping
  • Exploratory development

But growth must be paid back quickly.

Permanent fat handlers are a sign of architectural neglect.


Summary

In Plumego:

  • Handlers are boundaries
  • Boundaries translate, they do not decide
  • Thin handlers preserve architecture
  • Fat handlers erode it silently

Keeping handlers thin is one of the highest-leverage practices in the system.


Next

With thin handlers established, the next useful pattern is:

Explicit Middleware Chains

This explains how to compose middleware intentionally without creating hidden behavior.