DEV Community

amir taherkhani
amir taherkhani

Posted on Fully Autonomous

NestJS boolean queries: test missing, false, and invalid values separately

Reference examples: NestJS Skills documentation and source repository.

A request such as /items?includeArchived=false should exclude archived items. If the controller uses Boolean(includeArchived), it can return them anyway: JavaScript treats the nonempty string "false" as truthy.

The fix starts with an explicit contract. For this example:

  • An omitted parameter means false.
  • true includes archived items.
  • false excludes them.
  • Other strings produce HTTP 400.

That last rule matters. A misspelled value should not silently become a valid filter.

Why a boolean annotation is not enough

This is a conversion bug:

@Get()
list(@Query('includeArchived') includeArchived?: string) {
  return records.filter(
    item => Boolean(includeArchived) || !item.archived,
  );
}
Enter fullscreen mode Exit fullscreen mode

Boolean('false') and Boolean('yes') both return true. The conversion asks whether a value is truthy; it does not parse the words true and false.

Changing the TypeScript annotation to boolean does not define which wire values the API accepts. Runtime pipes and the application's transformation configuration determine that behavior.

Use an explicit parser at the HTTP boundary

Nest's pipes documentation describes parameter transformation and validation before the controller runs. For an endpoint receiving the original query value, use DefaultValuePipe followed by ParseBoolPipe:

import {
  Controller,
  DefaultValuePipe,
  Get,
  ParseBoolPipe,
  Query,
} from '@nestjs/common';

const records = [
  { id: 1, archived: false },
  { id: 2, archived: true },
];

@Controller('items')
export class ItemsController {
  @Get()
  list(
    @Query(
      'includeArchived',
      new DefaultValuePipe(false),
      ParseBoolPipe,
    )
    includeArchived: boolean,
  ) {
    return records.filter(
      item => includeArchived || !item.archived,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The default handles an omitted query. The parser handles supplied values. In Nest 11.2.7's implementation, ParseBoolPipe accepts booleans and the exact strings "true" and "false"; other values throw by default. An empty string is therefore different from an omitted parameter.

This example uses an in-memory list to make the behavior visible. A real service would receive the parsed boolean and apply the filter in its data-access code.

Check global transformation before copying the example

The parser must see a value that still preserves the caller's input.

In Nest 11.2.7's ValidationPipe, primitive boolean query transformation uses this expression:

return value === true || value === 'true';
Enter fullscreen mode Exit fullscreen mode

With a global ValidationPipe({ transform: true }), a boolean-typed query parameter can become false before its parameter-level parser runs. For example, yes can already be false when ParseBoolPipe receives it. The parser then accepts that boolean, and the intended 400 response is lost.

Inspect the existing bootstrap and pipe order. If global conversion already affects the parameter, preserve and validate the raw query value first, or use a query DTO that validates the allowed string values before converting them. Do not disable validation across the application to fix one endpoint.

The small reference fixture has no global transformation pipe. Its behavior should not be assumed for every application's configuration.

Test requests, not just the controller method

A direct call such as controller.list(false) skips parsing. Send actual HTTP requests through the initialized Nest application.

This test fragment assumes app is initialized with the application's normal bootstrap configuration and request is imported from supertest:

const active = [{ id: 1, archived: false }];
const all = [...active, { id: 2, archived: true }];

await request(app.getHttpServer())
  .get('/items')
  .expect(200, active);

await request(app.getHttpServer())
  .get('/items?includeArchived=false')
  .expect(200, active);

await request(app.getHttpServer())
  .get('/items?includeArchived=true')
  .expect(200, all);

for (const value of ['yes', '0', '', 'TRUE']) {
  await request(app.getHttpServer())
    .get(`/items?includeArchived=${value}`)
    .expect(400);
}
Enter fullscreen mode Exit fullscreen mode

The complete fixture contract includes application setup and cleanup. These are reference tests, not evidence that another application's endpoint passes.

If an existing public API accepts 1, 0, or mixed-case values, changing it to this stricter contract can break callers. Decide and document that compatibility change before altering the parser. Also test repeated query parameters with the HTTP adapter and query parser your application actually uses.

The useful regression test is the malformed request that must fail. It checks whether the whole request pipeline preserves your contract.

This article was generated with AI assistance and checked against the linked source code.

Top comments (0)