Skip to content

Introduce validation middleware#160

Draft
EmanFateen wants to merge 14 commits intokoala-ts:2.xfrom
EmanFateen:introduce-validation-middleware
Draft

Introduce validation middleware#160
EmanFateen wants to merge 14 commits intokoala-ts:2.xfrom
EmanFateen:introduce-validation-middleware

Conversation

@EmanFateen
Copy link
Copy Markdown
Contributor

@EmanFateen EmanFateen commented Apr 28, 2026

Q A
License GPLv3
Issue Closes #120

Hello,
In this PR, I introduced a validation middleware.Initially, I placed it under the http/middleware namespace because it is HTTP-related and functions as middleware. However, I later moved it to the validator namespace, as I felt it conceptually belongs there more. I don’t have a strong preference yet regarding its exact placement, but my current decision is to keep it under validator.
Please let me know if you have a different opinion.

@EmanFateen EmanFateen requested a review from imdhemy as a code owner April 28, 2026 08:24
Copy link
Copy Markdown
Member

@imdhemy imdhemy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pushing this forward. The general direction is useful, but I think the API needs one adjustment before this lands.

The key DevX goal should be:

  1. Framework users wire validation once with their configured validator.
  2. That validator already includes built-in and custom constraint validators.
  3. Endpoint code then creates middleware by passing only request validationRules.

So the preferred API shape is:

const validateRequest = createValidationMiddleware({ validate });

export const routes = [
  Route({
    method: 'POST',
    path: '/users',
    middleware: [
      validateRequest({
        email: ['notBlank', 'email'],
      }),
    ],
    handler: async scope => {
      scope.response.body = { ok: true };
    },
  }),
];

If custom response formatting is needed, the mapper can be wired once too:

const validateRequest = createValidationMiddleware({
  validate,
  mapViolations,
});

This keeps dependency injection explicit, supports custom constraints through the configured validator, avoids module mocking, and keeps endpoint-level code clean.

One important naming point: ValidationRules are the rules the request must satisfy, while validator constraints are the mechanics that perform validation. The middleware API should use validationRules for the endpoint argument to avoid mixing those concepts.

Comment thread src/validator/types.ts Outdated

export type Validator = (payload: Payload, rules: ValidationRules, options?: ValidateOptions) => Violation[];

export type ViolationMapper = (violations: Violation[]) => Record<string, string[]>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid adding ViolationMapper here.

src/validator/types.ts is exported through the validator public API, so this widens the framework API without proving that ViolationMapper is a user-facing contract.

If this type is only needed by the middleware factory, declare it in validation-middleware.ts. If we intentionally want users to customize violation mapping, expose it through the createValidationMiddleware options shape instead of adding a broad shared type by default.

This PR should stay focused on the middleware helper; removing barrels or reorganizing all validator types can be a separate refactor.

Comment thread src/validator/validation-middleware.ts Outdated
import { type HttpMiddleware } from '@/Http';
import { type ValidationRules, type Validator, type ViolationMapper } from '@/validator/types';

export function validationMiddleware(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The composition idea is good, but I think the API should make the one-time wiring explicit.

Instead of:

validationMiddleware(validate, mapViolations)(constraints)

prefer:

createValidationMiddleware({
  validate,
  mapViolations,
})(validationRules)

validate should be the user’s configured validator, which already includes built-in and custom constraint validators. That avoids hardcoding built-ins here while still giving endpoint code a simple API.

mapViolations can default to flattenViolations, since that is the framework default behavior required by the issue.

Comment thread src/validator/validation-middleware.ts Outdated
export function validationMiddleware(
validate: Validator,
mapViolations: ViolationMapper,
): (constraints: ValidationRules) => HttpMiddleware {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename this endpoint-level parameter from constraints to validationRules.

In this module there are two different concepts:

  • validationRules: the request rules an endpoint requires
  • constraint validators: the validation mechanics configured inside the validator

Using constraints here makes the public API ambiguous, especially because custom constraint validators are configured when creating the validator, not when creating endpoint middleware.

Comment thread src/validator/validation-middleware.ts Outdated
mapViolations: ViolationMapper,
): (constraints: ValidationRules) => HttpMiddleware {
return function createMiddleware(constraints: ValidationRules): HttpMiddleware {
return async function middleware(scope, next): Promise<void> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is contextually typed through the returned HttpMiddleware, so explicit scope and next types are not required for type safety.

I’m fine leaving it as-is, but if we want readability we can type them explicitly using the existing HTTP middleware types.


describe('Validation middleware', () => {
test('continues to the next middleware when no constrains are violated', async () => {
const validate: Validator = vi.fn().mockReturnValue({});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mock violates the Validator contract.

Validator returns Violation[], so the passing case should return [], not {}. Returning {} makes the test pass accidentally through .length being undefined, and it weakens the test as an example of the API.

@EmanFateen EmanFateen marked this pull request as draft April 29, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[validation] provide a validation middleware helper

2 participants