typescript 57 lines · 7 steps

Type-safe pagination queries in NestJS

A validated DTO turns raw query strings into safe pagination inputs and a structured paginated response.

Explained by highlit
1import { IsInt, IsOptional, Max, Min } from 'class-validator';
2import { Type } from 'class-transformer';
3 
4export class PaginationQueryDto {
5 @IsOptional()
6 @Type(() => Number)
7 @IsInt()
8 @Min(1)
9 page: number = 1;
10 
11 @IsOptional()
12 @Type(() => Number)
13 @IsInt()
14 @Min(1)
15 @Max(100)
16 limit: number = 20;
17 
18 get skip(): number {
19 return (this.page - 1) * this.limit;
20 }
21}
22 
23export interface PaginatedResult<T> {
24 data: T[];
25 meta: {
26 total: number;
27 page: number;
28 limit: number;
29 pageCount: number;
30 };
31}
32 
33@Controller('articles')
34export class ArticlesController {
35 constructor(private readonly articlesService: ArticlesService) {}
36 
37 @Get()
38 async findAll(
39 @Query() { page, limit, skip }: PaginationQueryDto,
40 ): Promise<PaginatedResult<Article>> {
41 const [data, total] = await this.articlesRepository.findAndCount({
42 order: { publishedAt: 'DESC' },
43 skip,
44 take: limit,
45 });
46 
47 return {
48 data,
49 meta: {
50 total,
51 page,
52 limit,
53 pageCount: Math.ceil(total / limit),
54 },
55 };
56 }
57}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Decorator-based DTOs let you validate and coerce query parameters declaratively instead of writing manual parsing logic.
  2. 2A computed getter on the DTO keeps derived values like the SQL offset next to the fields they depend on.
  3. 3Returning a generic result envelope gives every list endpoint a consistent, typed shape of data plus metadata.

Related explainers

typescript
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';

How a JWT strategy authenticates in NestJS

authentication jwt passport
Intermediate 7 steps
php
<?php
 
namespace App\Http\Controllers;
 

Streaming a filtered CSV export in Laravel

streaming csv-export query-builder
Intermediate 9 steps
typescript
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';

Refreshing auth tokens in an Angular interceptor

http-interceptor token-refresh rxjs
Advanced 8 steps
java
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
 
    @Query("""

Keyset pagination with Spring Data JPA

pagination cursor jpa
Advanced 8 steps
typescript
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';

How a GraphQL auth guard works in NestJS

authentication authorization jwt
Intermediate 7 steps
typescript
type ResizeCallback = (size: { width: number; height: number }) => void;
 
export function observeResize(callback: ResizeCallback, delay = 150) {
  let timeoutId: ReturnType<typeof setTimeout> | undefined;

Debouncing window resize in TypeScript

debounce closures event-listeners
Intermediate 6 steps

Share this explainer

Here's the card — post it anywhere.

Type-safe pagination queries in NestJS — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code