Query Parameters & Request Body
Parse and validate query strings and request bodies with Zod schemas
JustScale provides query() and body() plugins for validating query parameters and request bodies using Zod schemas.
Query Parameters
Accessing Raw Query Parameters
Query parameters are available as strings in the query object:
src/controllers/search.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
export const SearchController = createController('/search', {
routes: () => ({
search: Get('/').handle(({ query, res }) => {
// Request: /search?q=hello&limit=10
console.log(query.q); // "hello"
console.log(query.limit); // "10" (string!)
res.json({ query });
}),
}),
});⚠️
Warning
Query parameters are always strings. Use
query() for type conversion and validation.Using query() Plugin
The query() plugin validates and transforms query parameters using a Zod schema:
src/controllers/users.tsTypeScript
import { createController } from '@justscale/core';
import { Get, query } from '@justscale/http/builder';
import { PaginationSchema } from '../schemas/pagination';
export const UsersController = createController('/users', {
routes: () => ({
list: Get('/')
.apply(query(PaginationSchema))
.handle(({ query, res }) => {
// query is now typed as { page: number; limit: number }
const { page, limit } = query;
res.json({ page, limit });
}),
}),
});Example requests:
Bash
# With query params
curl "http://localhost:3000/users?page=2&limit=20"
# { "page": 2, "limit": 20 }
# Using defaults
curl "http://localhost:3000/users"
# { "page": 1, "limit": 10 }
# Invalid values
curl "http://localhost:3000/users?page=-1"
# { "error": "Validation error message" } - 400 Bad RequestQuery Validation Patterns
Search & Filtering
src/controllers/products.tsTypeScript
import { createController } from '@justscale/core';
import { Get, query } from '@justscale/http/builder';
import { SearchSchema } from '../schemas/search';
export const ProductsController = createController('/products', {
routes: () => ({
list: Get('/')
.apply(query(SearchSchema))
.handle(({ query, res }) => {
// query.q: string | undefined
// query.status: 'active' | 'inactive' | 'all'
// query.sort: 'name' | 'created' | 'updated'
res.json({ query });
}),
}),
});Boolean Flags
src/controllers/items.tsTypeScript
import { createController } from '@justscale/core';
import { Get, query } from '@justscale/http/builder';
import { FlagsSchema } from '../schemas/flags';
export const ItemsController = createController('/items', {
routes: () => ({
list: Get('/')
.apply(query(FlagsSchema))
.handle(({ query, res }) => {
// ?includeDeleted=true → query.includeDeleted = true
// ?includeDeleted=false → query.includeDeleted = false
// (no param) → query.includeDeleted = false (default)
res.json({ includeDeleted: query.includeDeleted });
}),
}),
});Date Ranges
src/controllers/reports.tsTypeScript
import { createController } from '@justscale/core';
import { Get, query } from '@justscale/http/builder';
import { DateRangeSchema } from '../schemas/date-range';
export const ReportsController = createController('/reports', {
routes: () => ({
list: Get('/')
.apply(query(DateRangeSchema))
.handle(({ query, res }) => {
// ?startDate=2024-01-01&endDate=2024-12-31
// query.startDate: Date | undefined
// query.endDate: Date | undefined
res.json({
start: query.startDate?.toISOString(),
end: query.endDate?.toISOString(),
});
}),
}),
});Request Body
Accessing Raw Body
The request body is automatically parsed as JSON and available in the body property:
src/controllers/users.tsTypeScript
import { createController } from '@justscale/core';
import { Post } from '@justscale/http';
export const UsersController = createController('/users', {
routes: () => ({
create: Post('/').handle(({ body, res }) => {
// body is typed as 'unknown'
console.log(body); // { name: "Alice", email: "alice@example.com" }
res.json({ received: body });
}),
}),
});⚠️
Warning
Raw
rawBody is typed as unknown. Always validate it before use!Using body() Plugin
The body() plugin validates the request body against a Zod schema and exposes it as typed body in the context:
src/controllers/users.tsTypeScript
import { createController } from '@justscale/core';
import { Post, body } from '@justscale/http/builder';
import { UserService } from '../services/user-service';
import { CreateUserBody, UserResponse, ErrorResponse } from '../schemas/user';
export const UsersController = createController('/users', {
inject: { users: UserService },
routes: (services) => ({
create: Post('/')
.apply(body(CreateUserBody))
.returns(UserResponse, 201)
.returns(ErrorResponse, 400) // Validation errors
.handle(async ({ body, res }) => {
// body is typed as { name: string; email: string; age?: number }
const user = await services.users.create(body);
res.status(201).json({ user });
}),
}),
});Example requests:
Bash
# Valid request
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com","age":25}'
# 201 Created
# Invalid email
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"invalid"}'
# { "error": "Invalid email" } - 400 Bad Request
# Missing required field
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com"}'
# { "error": "Name is required" } - 400 Bad RequestValidation Patterns
Nested Objects
src/controllers/posts.tsTypeScript
import { createController } from '@justscale/core';
import { Post, body } from '@justscale/http/builder';
import { CreatePostSchema } from '../schemas/post';
export const PostsController = createController('/posts', {
routes: () => ({
create: Post('/')
.apply(body(CreatePostSchema))
.handle(({ body, res }) => {
// body.author.name, body.author.email, body.tags are all typed
res.status(201).json({ post: body });
}),
}),
});Optional Fields with Defaults
src/controllers/players.tsTypeScript
import { createController } from '@justscale/core';
import { Post, body } from '@justscale/http/builder';
import { CreatePlayerSchema } from '../schemas/player';
export const PlayersController = createController('/players', {
routes: () => ({
create: Post('/')
.apply(body(CreatePlayerSchema))
.handle(({ body, res }) => {
// If chips is not provided, defaults to 1000
// If isActive is not provided, defaults to true
res.status(201).json({ player: body });
}),
}),
});Union Types
src/controllers/profile.tsTypeScript
import { createController } from '@justscale/core';
import { Put, body } from '@justscale/http/builder';
import { UpdateUserSchema } from '../schemas/profile';
export const ProfileController = createController('/profile', {
routes: () => ({
update: Put('/')
.apply(body(UpdateUserSchema))
.handle(({ body, res }) => {
// TypeScript knows body.type is either 'email' or 'password'
if (body.type === 'email') {
// body.email is available
} else {
// body.currentPassword and body.newPassword are available
}
res.json({ updated: true });
}),
}),
});Combining Query and Body
You can use both query() and body() in the same route:
src/controllers/users.tsTypeScript
import { createController } from '@justscale/core';
import { Post, query, body } from '@justscale/http/builder';
import { UserService, sendWelcomeEmail } from '../services/user-service';
import { QuerySchema, CreateUserBody, UserResponse, ErrorResponse } from '../schemas/user';
export const UsersController = createController('/users', {
inject: { users: UserService },
routes: (services) => ({
create: Post('/')
.apply(query(QuerySchema))
.apply(body(CreateUserBody))
.returns(UserResponse, 201)
.returns(ErrorResponse, 400)
.handle(async ({ query, body, res }) => {
// query.notify: boolean
// body.name: string
// body.email: string
const user = await services.users.create(body);
if (query.notify) {
sendWelcomeEmail(user.email);
}
res.status(201).json({ user });
}),
}),
});Error Handling
When validation fails, query() or body() automatically:
- Returns a 400 Bad Request status with field-level errors
- Stops execution before reaching the handler
ℹ️
Info
The validation error messages come directly from Zod. Customize them using Zod's error message options.
src/controllers/users.tsTypeScript
import { createController } from '@justscale/core';
import { Post, body } from '@justscale/http/builder';
import { UserSchema } from '../schemas/user';
export const UsersController = createController('/users', {
routes: () => ({
create: Post('/')
.apply(body(UserSchema))
.handle(({ body, res }) => {
// This handler only runs if validation succeeds
res.status(201).json({ user: body });
}),
}),
});