Query Validation
Fossyl provides three levels of parameter validation: URL path parameters, query string parameters, and pagination parameters. Each is validated at a different point in the request lifecycle and has different type inference behavior.
URL Parameters
Section titled “URL Parameters”URL parameters are declared using :param syntax in the route path. Fossyl parses them at the type level — no runtime configuration needed.
// URL params are declared with :param syntax in the path// Fossyl extracts them at the type level automaticallyimport { createRouter<BasePath extends string>(_: BasePath) => Router<BasePath> } from "@fossyl/core";
const routerRouter<"/api"> = createRouter<BasePath extends string>(_: BasePath) => Router<BasePath><"/api">("/api");
const _getUserRouteRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api"> .createEndpoint<Path extends `/api${string}`>(path: Path) => Endpoint<Path, true>("/api/users/:id") .get<Response extends ResponseData>(
handler: (
parameters: { url: { id: string } & { readonly __kind: "url" } } & {
readonly __kind: "parameters"
},
) => () => Promise<Response>,
) => Route(({ url{ id: string } & { readonly __kind: "url" } }) => async () => { // url is typed as: { id: string } const userIdstring = url{ id: string } & { readonly __kind: "url" }.idstring; return { typeName: "User" as const, id: userIdstring }; });
// Multiple params work tooconst _getCommentRouteRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api"> .createEndpoint<Path extends `/api${string}`>(path: Path) => Endpoint<Path, true>("/api/posts/:postId/comments/:commentId") .get<Response extends ResponseData>(
handler: (
parameters: { url: { id: string } & { readonly __kind: "url" } } & {
readonly __kind: "parameters"
},
) => () => Promise<Response>,
) => Route(({ url{ id: string } & { readonly __kind: "url" } }) => async () => { // url is typed as: { postId: string; commentId: string } return { typeName: "Comment" as const, postId: url{ id: string } & { readonly __kind: "url" }.postIdstring, commentId: url{ id: string } & { readonly __kind: "url" }.commentIdstring, }; });URL params are always string types since they come from the URL path. The Params<Path> utility type drives this inference.
Path Pattern Rules
Section titled “Path Pattern Rules”:id→{ id: string }:postId/comments/:commentId→{ postId: string; commentId: string }- Static segments and dynamic params can be mixed freely
Query Parameters
Section titled “Query Parameters”Query parameters are optional and require a queryValidator. This keeps the type system honest — you explicitly declare what query params you expect.
// Query params are validated and typed via queryValidatorimport { createRouter<BasePath extends string>(_: BasePath) => Router<BasePath> } from "@fossyl/core";
const routerRouter<"/api"> = createRouter<BasePath extends string>(_: BasePath) => Router<BasePath><"/api">("/api");
const _searchRouteRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api"> .createEndpoint<Path extends `/api${string}`>(path: Path) => Endpoint<Path, true>("/api/search") .query{ q: string; limit?: number; offset?: number } & {
readonly __kind: "query"
}((data): { qstring: string; limitnumber | undefined?: number; offsetnumber | undefined?: number } => { const paramsRecord<string, string | undefined> = data as Record<string, string | undefinedundefined>; if (!paramsRecord<string, string | undefined>.qstring || paramsRecord<string, string | undefined>.qstring.trim() => string() === "") { throw new Error('Search query "q" is required'); } return { q: paramsRecord<string, string | undefined>.qstring, limit: paramsRecord<string, string | undefined>.limitnumber | undefined ? Number(paramsRecord<string, string | undefined>.limitnumber | undefined) : undefinedundefined, offset: paramsRecord<string, string | undefined>.offsetnumber | undefined ? Number(paramsRecord<string, string | undefined>.offsetnumber | undefined) : undefinedundefined, }; }) .get<Response extends ResponseData>(
handler: (
parameters: {
query: { q: string; limit?: number; offset?: number } & {
readonly __kind: "query"
}
} & { readonly __kind: "parameters" },
) => () => Promise<Response>,
) => Route(({ query{ q: string; limit?: number; offset?: number } & {
readonly __kind: "query"
} }) => async () => { // query is typed as: { q: string; limit?: number; offset?: number } const results = await searchDatabase(query{ q: string; limit?: number; offset?: number } & {
readonly __kind: "query"
}.qstring, query{ q: string; limit?: number; offset?: number } & {
readonly __kind: "query"
}.limitnumber | undefined, query{ q: string; limit?: number; offset?: number } & {
readonly __kind: "query"
}.offsetnumber | undefined); return { typeName: "SearchResult" as const, results }; });Query Validation with Zod
Section titled “Query Validation with Zod”The @fossyl/zod package provides zodQueryValidator for Zod-based query validation. Use z.coerce for type coercion from strings:
// Query validation with Zod — same pattern as body validationimport { z } from "zod";import { zodQueryValidator<T extends ZodTypeAny>(schema: T) => T["parse"] } from "@fossyl/zod";import { createRouter<BasePath extends string>(_: BasePath) => Router<BasePath> } from "@fossyl/core";
const routerRouter<"/api"> = createRouter<BasePath extends string>(_: BasePath) => Router<BasePath><"/api">("/api");
const searchSchema = z.object({ q: z.string().min(1, "Search query is required"), limit: z.coerce.number().int().min(1).max(100).optional().default(20), offset: z.coerce.number().int().min(0).optional().default(0),});
const _searchRouteRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api"> .createEndpoint<Path extends `/api${string}`>(path: Path) => Endpoint<Path, true>("/api/search") .query{ readonly __kind: "query" }(zodQueryValidator<T extends ZodTypeAny>(schema: T) => T["parse"](searchSchema)) .get<Response extends ResponseData>(
handler: (
parameters: { query: { readonly __kind: "query" } } & {
readonly __kind: "parameters"
},
) => () => Promise<Response>,
) => Route(({ query{ readonly __kind: "query" } }) => async () => { // query is typed as: { q: string; limit: number; offset: number } return { typeName: "SearchResult" as const, results: await searchDb(query{ readonly __kind: "query" }) }; });Query vs URL Params
Section titled “Query vs URL Params”| Aspect | URL Params | Query Params |
|---|---|---|
| Syntax | :id in path | ?key=value in URL |
| Required | Always | Optional |
| Type | Always string | Any (coerced) |
| Validation | Automatic | Via queryValidator |
| Access | url.paramName | query.fieldName |
Pagination Parameters
Section titled “Pagination Parameters”List routes handle pagination automatically. The route type provides page, pageSize, and configures defaults and limits.
// Paginated routes automatically handle pagination params:// ?page=1&pageSize=20 → { page: 1, pageSize: 20 }import { createRouter<BasePath extends string>(_: BasePath) => Router<BasePath> } from "@fossyl/core";
const routerRouter<"/api"> = createRouter<BasePath extends string>(_: BasePath) => Router<BasePath><"/api">("/api");
const _listPostsRouteRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api"> .createEndpoint<Path extends `/api${string}`>(path: Path) => Endpoint<Path, true>("/api/posts") .paginate(
paginationConfig: PaginationConfig,
) => {
validator: <RequestBody extends unknown>(
validatorFunction: ValidatorFunction<RequestBody>,
) => {
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}
authenticator: <Auth extends Authentication>(
authenticationFunction: AuthenticationFunction<Auth>,
) => {
validator: <RequestBody extends unknown>(
validatorFunction: ValidatorFunction<RequestBody>,
) => {
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}
} & {
get: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
delete: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}
} & {
get: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
delete: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}({ defaultPageSize: 20, maxPageSize: 100, }) .get<Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route(({ pagination{ page: number; pageSize: number; hasMore: boolean } }) => async () => { // pagination is typed as: { page: number; pageSize: number } const { pagenumber, pageSizenumber } = pagination{ page: number; pageSize: number; hasMore: boolean };
// N+1 trick: fetch one extra to determine hasMore const items = await db .selectFrom("posts") .limit(pageSizenumber + 1) .offset((pagenumber - 1) * pageSizenumber) .execute();
const hasMoreboolean = items.length > pageSizenumber;
return { data: hasMoreboolean ? items.slice(0, pageSizenumber) : items, pagination: { pagenumber, pageSizenumber, hasMoreboolean, }, }; });
// Authenticated list routes work tooconst _listMyPostsRouteRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api"> .createEndpoint<Path extends `/api${string}`>(path: Path) => Endpoint<Path, true>("/api/my-posts") .paginate(
paginationConfig: PaginationConfig,
) => {
validator: <RequestBody extends unknown>(
validatorFunction: ValidatorFunction<RequestBody>,
) => {
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}
authenticator: <Auth extends Authentication>(
authenticationFunction: AuthenticationFunction<Auth>,
) => {
validator: <RequestBody extends unknown>(
validatorFunction: ValidatorFunction<RequestBody>,
) => {
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}
} & {
get: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
delete: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}
} & {
get: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
delete: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}({ defaultPageSize: 20, maxPageSize: 100, }) .authenticator<Auth extends Authentication>(
authenticationFunction: AuthenticationFunction<Auth>,
) => {
validator: <RequestBody extends unknown>(
validatorFunction: ValidatorFunction<RequestBody>,
) => {
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}
} & {
get: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
delete: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}(myAuth) .get<Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route(({ pagination{ page: number; pageSize: number; hasMore: boolean } }) => (authAuthentication & { readonly __kind: "auth" }) => async () => { const items = await db .selectFrom("posts") .where("userId", "=", authAuthentication & { readonly __kind: "auth" }.userId) .limit(pagination{ page: number; pageSize: number; hasMore: boolean }.pageSizenumber + 1) .offset((pagination{ page: number; pageSize: number; hasMore: boolean }.pagenumber - 1) * pagination{ page: number; pageSize: number; hasMore: boolean }.pageSizenumber) .execute();
return { data: items.slice(0, pagination{ page: number; pageSize: number; hasMore: boolean }.pageSizenumber), pagination: { page: pagination{ page: number; pageSize: number; hasMore: boolean }.pagenumber, pageSize: pagination{ page: number; pageSize: number; hasMore: boolean }.pageSizenumber, hasMore: items.length > pagination{ page: number; pageSize: number; hasMore: boolean }.pageSizenumber, }, }; });PaginationConfig
Section titled “PaginationConfig”type PaginationConfig = { defaultPageSizenumber: number; // Default when ?pageSize is omitted maxPageSizenumber: number; // Hard upper limit enforced at runtime};Response Shape
Section titled “Response Shape”Paginated endpoints return a PaginatedResponse<T>:
type PaginatedResponse<T> = { dataT: T[]; pagination{ page: number; pageSize: number; hasMore?: boolean; total?: number }: { pagenumber: number; pageSizenumber: number; hasMoreboolean | undefined?: boolean; // Use N+1 trick to determine totalnumber | undefined?: number; // Optional: include if you do a count query };};The N+1 trick (fetching pageSize + 1 items) is the recommended way to determine hasMore without a separate count query.