<!-- Markdown mirror of https://justscale.sh/docs/http/query-body -->

# 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

```typescript
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

```typescript
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('/')
      .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

```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 Request
```

## Query Validation Patterns

### Search & Filtering

src/controllers/products.tsTypeScript

```typescript
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('/')
      .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

```typescript
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('/')
      .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

```typescript
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('/')
      .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

```typescript
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

```typescript
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('/')
      .body(CreateUserBody)
      .returns(201, UserResponse)
      .returns(400, ErrorResponse)  // 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

```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 Request
```

## Validation Patterns

### Nested Objects

src/controllers/posts.tsTypeScript

```typescript
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('/')
      .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

```typescript
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('/')
      .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

```typescript
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('/')
      .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

```typescript
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('/')
      .query(QuerySchema)
      .body(CreateUserBody)
      .returns(201, UserResponse)
      .returns(400, ErrorResponse)
      .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

```typescript
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('/')
      .body(UserSchema)
      .handle(({ body, res }) => {
        // This handler only runs if validation succeeds
        res.status(201).json({ user: body });
      }),
  }),
});
```

## Next Steps

- Response Types
- Validation
- Middleware
