Skip to Content
Course content

189: Request Validation Middleware

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this pattern a hundred times in production codebases. A developer starts with one or two endpoints, and they handle validation right there in the route handler. It seems fine at first. But then the app grows to fifty endpoints, and suddenly the "business logic" is buried under a mountain of if-statements checking for nulls, empty strings, or negative numbers.

case class TransferRequest(fromAccount: String, toAccount: String, amount: Double)

// This is the "manual" way that eventually breaks everything
def handleTransfer(req: Request[TransferRequest]): Response = {
  val body = req.body
  
  if (body.fromAccount.isEmpty) {
    return Response.BadRequest("From account is required")
  }
  if (body.toAccount.isEmpty) {
    return Response.BadRequest("To account is required")
  }
  if (body.amount <= 0) {
    return Response.BadRequest("Amount must be positive")
  }
  
  // Finally, the actual logic
  bankingService.transfer(body.fromAccount, body.toAccount, body.amount)
  Response.Ok("Transfer successful")
}

The Repetition Trap

The problem here isn't that the code is "wrong"—it works. The problem is that this logic is now coupled to the handler. If you add a handleWithdrawal or a handleDeposit endpoint, you're going to copy-paste those if blocks. Eventually, someone will forget to check if the amount is positive in one of those endpoints, and you'll end up with a bug where users can "withdraw" negative money to effectively deposit it into their account. I've literally seen that happen in a fintech app I consulted for.

We need to move this validation "up the chain" so the handler only ever receives a request that is already guaranteed to be valid. In Scala, we can do this using middleware—a function that wraps our handler.

Short-Circuiting with Validation Middleware

Instead of letting the request hit the handler and then checking its validity, we create a middleware layer that intercepts the request. If the validation fails, the middleware "short-circuits" the request and returns a BadRequest immediately. The handler never even knows the request existed.

case class ValidatedRequest[T](data: T)

// A generic validation middleware
def withValidation[T](validator: T => Either[String, T])(handler: Request[ValidatedRequest[T]] => Response): Request[T] => Response = {
  request => {
    validator(request.body) match {
      case Left(error) => 
        Response.BadRequest(error)
      case Right(validData) => 
        handler(Request(ValidatedRequest(validData)))
    }
  }
}

// Now we define a specific validator for our TransferRequest
val transferValidator: TransferRequest => Either[String, TransferRequest] = req => {
  if (req.fromAccount.isEmpty) Left("From account is required")
  else if (req.toAccount.isEmpty) Left("To account is required")
  else if (req.amount <= 0) Left("Amount must be positive")
  else Right(req)
}

// The handler is now clean. It only deals with the "Happy Path".
def handleTransfer(req: Request[ValidatedRequest[TransferRequest]]): Response = {
  val body = req.body.data
  bankingService.transfer(body.fromAccount, body.toAccount, body.amount)
  Response.Ok("Transfer successful")
}

// Wiring it together
val route = withValidation(transferValidator)(handleTransfer)

Notice how handleTransfer now takes a ValidatedRequest instead of a raw TransferRequest. This is a powerful trick. By changing the type, I'm using the Scala compiler to prove that this handler cannot be called unless the validation middleware has already run. You can't accidentally skip the validation because the types won't match.

I prefer this approach because it separates what the request should look like from what the system should do with it. Your handlers stay lean, and your validation logic is centralized and reusable. If the business decides that amount must now be at least 1.00, you change it in one validator, and every single endpoint using that middleware is instantly updated.




📋 Practical Task

Build a Multi-Header Security and Versioning Middleware

You are building a public API that requires two things before any request is processed: a valid X-API-Key (which should be "secret-key-123" for this exercise) and an X-API-Version header (which must be exactly "v2").

Create a middleware function called withSecurityHeaders. This middleware should check the headers of an incoming Request:

  • If X-API-Key is missing or incorrect, return a Response.Unauthorized("Invalid API Key").
  • If X-API-Version is missing or not "v2", return a Response.BadRequest("Unsupported API Version").
  • If both are present and correct, pass the request through to the inner handler.

Test your middleware by wrapping a simple "Hello World" handler. Ensure that requests with missing or incorrect headers are rejected before they ever reach the handler logic.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.