Supporting query parameters schema ​
In the previous example, we reduced path parameters to "John" and "Michelle". We will use the same schema, for query parameters this time.
Redefining our greet
handler ​
ts
// src/server-first/3-support-query-schema/greet-handler.tsx
/** @jsxImportSource hono/jsx */
import { delay } from '@/utils/delay'
import * as x from 'unhoax'
import { HandlerBuilder } from './handler-builder'
import { respondWith } from '../definition/response'
import { HandlerBuilder } from './handler-builder'
export const greetHandler = HandlerBuilder.get('/hello/:name')
.params({ name: x.literal('John', 'Michelle') }, () => {
export const greetHandler = HandlerBuilder.get('/hello')
.query({ name: x.literal('John', 'Michelle') }, () => {
return respondWith
.status(400)
.html(<div style="color: red">Name must be “John” or “Michelle”</div>)
})
.handleWith(async ({ params }) => {
.handleWith(async ({ query }) => {
await delay(150) // mimic DB access.
return respondWith.html(<div style="color: blue">Hello, {params.name}</div>)
return respondWith.html(<div style="color: blue">Hello, {query.name}</div>)
})
Define query validation in the HandlerBuilder
​
ts
// src/server-first/3-support-query-schema/handler-builder.ts
import * as x from 'unhoax'
import { PathParameters } from '../definition/PathParameters'
import { HttpMethod } from '../definition/html-route'
import { Handler, HandlerInput } from './handler'
function createHandlerBuilder(method: HttpMethod) {
return <Path extends `/${string}`>(path: Path) => {
let paramsGuard: GuardConfig | undefined
let queryGuard: GuardConfig | undefined
const builder: HandlerBuilder<Path, PathParameters<Path>> = {
params(props, onInvalid) {
paramsGuard = { schema: x.object(props), onInvalid }
return builder as any
},
query(props, onInvalid) {
queryGuard = { schema: x.object(props), onInvalid }
return builder as any
},
handleWith(handle) {
return {
method,
path,
handle: async (input) => {
const params = guardWith(paramsGuard, input.params, input.params)
if (!params.success) return params.error
const query = guardWith(queryGuard, input.query, {})
if (!query.success) return query.error
return handle({
...input,
params: params.value as any,
query: query.value as any,
})
},
}
},
}
return builder
}
}
export const HandlerBuilder = {
get: createHandlerBuilder('GET'),
post: createHandlerBuilder('POST'),
}
interface HandlerBuilder<
Path,
Params,
Query = Record<string, any>,
Body = undefined,
> {
params<P extends Record<string, any>>(
props: x.PropsOf<P>,
onInvalid: OnInvalid,
): HandlerBuilder<Path, P, Query, Body>
query<Q extends Record<string, any>>(
props: x.PropsOf<Q>,
onInvalid: OnInvalid,
): HandlerBuilder<Path, Params, Q, Body>
handleWith(
handler: (input: HandlerInput<Params, Query, Body>) => Promise<Response>,
): Handler<Path, Params, Query, Body>
}
type OnInvalid = (error: x.ParseError) => Response | Promise<Response>
interface GuardConfig {
schema: x.Schema<unknown>
onInvalid: OnInvalid
}
type GuardResult =
| { success: false; error: ReturnType<OnInvalid> }
| { success: false; error: Response | Promise<Response> }
| { success: true; value: unknown }
function guardWith<F>(
config: GuardConfig | undefined,
value: unknown,
fallback: F,
): GuardResult {
if (!config) return { success: true, value: fallback }
const result = config.schema.parse(value)
return result.success
? result
: { success: false, error: config.onInvalid(result.error) }
}
Unit Testing ​
ts
// src/server-first/3-support-query-schema/greet-handler.spec.ts
import { describe, expect, it } from 'vitest'
import { greetHandler } from './greet-handler'
describe('greetHandler – with params', () => {
it('fails with 400 when params are invalid', async () => {
describe('greetHandler – with query', () => {
it('fails with 400 when query is invalid', async () => {
const result = await greetHandler.handle({
params: { name: 'Toto' },
params: {},
body: undefined,
query: {},
query: { name: 'Toto' },
headers: new Headers(),
})
expect(result.status).toBe(400)
expect(await result.text()).toBe(
'<div style="color: red">Name must be “John” or “Michelle”</div>',
)
})
it('responds with 200 & a blue div', async () => {
const result = await greetHandler.handle({
params: { name: 'John' },
params: {},
body: undefined,
query: {},
query: { name: 'John' },
headers: new Headers(),
})
expect(result.status).toBe(200)
expect(await result.text()).toBe(
'<div style="color: blue">Hello, John</div>',
)
})
})
Result:
sh
âś“ server-first/3-support-query-schema/greet-handler.spec.ts (2)
✓ greetHandler – with query (2)
âś“ fails with 400 when query is invalid
âś“ responds with 200 & a blue div
End-to-End Testing ​
Now we can boot the server:
sh
npx tsx ./src/server-first/3-support-query-schema/server.ts
Let’s test with "John" and "Jack":
sh
$ curl 'http://localhost:6600/hello?name=Jack'
<div style="color: red">Name must be “John” or “Michelle”</div>
# âś…
curl 'http://localhost:6600/hello?name=John'
<div style="color: blue">Hello, John</div>
# âś…
All good!