Turtor Docs
API

Paginace

Stránkovaná odpověď API -- DTOs, implementace v service a použití na frontendu

API používá page-based paginaci (1-indexovanou) se standardizovaným formátem požadavku a odpovědi.

Formát odpovědi

interface PaginatedResponseDto<T> {
  data: T[];
  meta: {
    page: number;       // Aktuální stránka (1-indexovaná)
    limit: number;      // Položek na stránku
    total: number;      // Celkový počet položek
    totalPages: number; // Celkový počet stránek
  };
}

Pagination DTOs

Umístění: apps/api/src/common/dto/pagination.dto.ts

PaginationDto

class PaginationDto {
  @Type(() => Number)
  @Min(1)
  page?: number = 1;       // 1-indexovaná, výchozí: 1

  @Type(() => Number)
  @Min(1)
  @Max(100)
  limit?: number = 20;     // Výchozí: 20, maximum: 100
}

Backend vynucuje @Max(100) na parametru limit. Požadavky s limit > 100 selžou s validační chybou.

SortingDto

class SortingDto {
  sortBy?: string = 'createdAt';
  sortOrder?: 'ASC' | 'DESC' = 'DESC';
}

PaginatedSortedQueryDto

Kombinace paginace a řazení:

class PaginatedSortedQueryDto extends PaginationDto {
  sortBy?: string = 'createdAt';
  sortOrder?: 'ASC' | 'DESC' = 'DESC';
}

DateRangeDto

class DateRangeDto {
  dateFrom?: string;    // ISO datum
  dateTo?: string;      // ISO datum
}

Implementace v service

import { createPaginatedResponse, PaginatedResponseDto } from '../../common/dto';

async findAll(params: PaginationDto): Promise<PaginatedResponseDto<Feature>> {
  const { page = 1, limit = 20 } = params;
  const skip = (page - 1) * limit;

  const [data, total] = await this.repository.findAndCount({
    skip,
    take: limit,
    order: { createdAt: 'DESC' },
  });

  return createPaginatedResponse({ data, total, page, limit });
}

Helper createPaginatedResponse() automaticky vypočítá totalPages:

function createPaginatedResponse<T>({ data, total, page, limit }) {
  return {
    data,
    meta: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
    },
  };
}

Swagger dekorátor

import { ApiPaginatedResponse } from '../../common/dto';

@Get()
@ApiPaginatedResponse(Feature, 'Stránkovaný seznam záznamů')
findAll(@Query() params: PaginationDto): Promise<PaginatedResponseDto<Feature>> {
  return this.service.findAll(params);
}

Použití na frontendu

S Orval hooky

import { useCoursesControllerFindAllApiV1 } from '@turtor/api-types/courses';

function CourseList() {
  const [page, setPage] = useState(1);

  const { data, isLoading } = useCoursesControllerFindAllApiV1({
    page,
    limit: 25,
    status: 'published',
  });

  const courses = data?.data ?? [];
  const totalPages = data?.meta.totalPages ?? 1;

  return (
    <>
      {courses.map(course => <CourseCard key={course.id} course={course} />)}
      <Pagination page={page} totalPages={totalPages} onChange={setPage} />
    </>
  );
}

S useTablePagination hookem

Pro tabulky s URL-synchronizovanou paginací:

import { useTablePagination } from '~/hooks';

const {
  page, limit, sortBy, sortOrder,  // Pro API volání (1-indexované)
  pagination, sorting,              // Pro TanStack Table (0-indexované)
  onPaginationChange, onSortingChange,
  getResultsRange, resetToFirstPage,
} = useTablePagination({ defaultLimit: 25 });

const { data, meta, isFetching } = useCourseList({
  page, limit, sortBy, sortOrder,
});

useTablePagination automaticky:

  • Synchronizuje stav stránkování s URL parametry
  • Převádí mezi 0-indexovaným TanStack Table a 1-indexovaným API
  • Poskytuje getResultsRange() pro zobrazení "Zobrazeno 1-25 z 150"

Důležité poznámky

  • Stránkování je 1-indexované -- první stránka je page=1, ne page=0
  • Výchozí limit je 20 -- pokud není specifikován, API vrátí 20 položek
  • Maximum je 100 -- limit > 100 způsobí validační chybu 400
  • Orval hooky nerozbalují odpověď -- data z hooku obsahuje celý objekt { data: T[], meta }, ne jen pole
  • Implicitní konverze typů -- díky enableImplicitConversion se query string ?page=2 automaticky převede na číslo