Building Modern Backends with Kaapi: Request validation Part 2

Published: (January 11, 2026 at 02:33 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

This series is written for backend developers who love TypeScript and can appreciate Hapi’s design philosophy.

Validation… again?

Maybe you followed along with the previous article on request validation using Joi, ArkType, Valibot, or Zod.

You’ve got a shiny Kaapi app, routes are clean, validation is solid. Everything felt neat and reassuring.

You probably started with something like this:

import { z } from 'zod';

app
  .base()
  .zod({
    params: z.object({
      name: z.string().min(3).max(10)
    })
  })
  .route({
    method: 'GET',
    path: '/hello/{name}',
    handler: function (request, h) {
      return `Hello ${request.params.name}!`;
    }
  });

And honestly?
That works perfectly fine.

Until the day your app stops being small.

At some point, routes start multiplying. You stop defining them inline. They live in their own files, grouped by feature. And suddenly, that nice little chain you had doesn’t quite fit anymore.

So the question becomes:

How do you keep validation close to your routes… when routes are no longer close to your app?

That’s what we’re solving here.

The new setup (you’ve probably done this already)

You want two things:

  1. Routes defined independently – a single place where they’re registered with app.route(...).
  2. Validation, type safety, and freedom to choose your favorite validator.

Let’s walk through it, one validator at a time.

Joi: the familiar path

If you’re using Joi, nothing changes. That’s intentional.

npm i joi
import { Kaapi, KaapiServerRoute } from '@kaapi/kaapi';
import Joi from 'joi';

const app = new Kaapi({ /* ... */ });

const route: KaapiServerRoute = {
  method: 'GET',
  path: '/hello/{name}',
  options: {
    validate: {
      params: Joi.object({
        name: Joi.string().min(3).max(10).required()
      })
    }
  },
  handler: function (request, h) {
    return `Hello ${request.params.name}!`;
  }
};

app.route(route);

No magic. No surprises.
If you’ve used Hapi before, this should feel like coming home.

Zod: validation that talks to TypeScript

Now let’s say you want strong type inference without juggling generics.

npm i zod @kaapi/validator-zod

Extend Kaapi

import { Kaapi } from '@kaapi/kaapi';
import { validatorZod } from '@kaapi/validator-zod';

const app = new Kaapi({ /* ... */ });

await app.extend(validatorZod);

Define the route

import { withSchema } from '@kaapi/validator-zod';
import { z } from 'zod';

const route = withSchema({
  params: z.object({
    name: z.string().min(3).max(10)
  })
}).route({
  method: 'GET',
  path: '/hello/{name}',
  handler: function (request, h) {
    return `Hello ${request.params.name}!`;
  }
});

app.route(route);

No generics. Your types come straight from the schema. You write validation once and TypeScript follows along.

Valibot: same idea, different flavor

Valibot plays in the same league as Zod, with a slightly different syntax.

npm i valibot @kaapi/validator-valibot

Extend Kaapi

import { Kaapi } from '@kaapi/kaapi';
import { validatorValibot } from '@kaapi/validator-valibot';

const app = new Kaapi({ /* ... */ });

await app.extend(validatorValibot);

Define the route

import { withSchema } from '@kaapi/validator-valibot';
import * as v from 'valibot';

const route = withSchema({
  params: v.object({
    name: v.pipe(v.string(), v.minLength(3), v.maxLength(10))
  })
}).route({
  method: 'GET',
  path: '/hello/{name}',
  handler: function (request, h) {
    return `Hello ${request.params.name}!`;
  }
});

app.route(route);

ArkType: schemas you can read

npm i arktype @kaapi/validator-arktype

Extend Kaapi

import { Kaapi } from '@kaapi/kaapi';
import { validatorArk } from '@kaapi/validator-arktype';

const app = new Kaapi({ /* ... */ });

await app.extend(validatorArk);

Define the route

import { withSchema } from '@kaapi/validator-arktype';
import { type } from 'arktype';

const route = withSchema({
  params: type({
    name: '3 <= string <= 10'
  })
}).route({
  method: 'GET',
  path: '/hello/{name}',
  handler: function (request, h) {
    return `Hello ${request.params.name}!`;
  }
});

app.route(route);

What actually matters here

Regardless of the validator:

  • Routes are defined independently.
  • Each route picks the validator it wants.
  • Registration stays dead simple:
app.route(route);
  • Joi works out of the box.
  • Zod, Valibot, and ArkType plug in via withSchema.

Same app. Same API. Different tools.

So… which validator should you use?

GoalRecommended validator
Minimal codeJoi or ArkType
Strong type inference without genericsZod or Valibot
Human‑readable schemasArkType
Great auto‑docsZod, Valibot, or Joi

Kaapi doesn’t force a choice.
It gives you the whole toolbox. Pick the screwdriver you like using.

Source code

Want the full example?

👉 github.com/kaapi/kaapi‑examples

shygyver/kaapi-monorepo-playground

The example lives under validation-app.

Check the README for run instructions.

More Kaapi articles are coming.

This one was just about keeping validation where it belongs.

📦 Get started now

npm install @kaapi/kaapi

🔗 Learn more: Kaapi Wiki

Back to Blog

Related posts

Read more »

My Node.js API Best Practices in 2025

Node.js has been powering production APIs for well over a decade now, and in 2025 it’s no longer “new” or experimental, it’s infrastructure. That maturity has c...