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, nepage=0 - Výchozí limit je 20 -- pokud není specifikován, API vrátí 20 položek
- Maximum je 100 --
limit > 100způsobí validační chybu 400 - Orval hooky nerozbalují odpověď --
dataz hooku obsahuje celý objekt{ data: T[], meta }, ne jen pole - Implicitní konverze typů -- díky
enableImplicitConversionse query string?page=2automaticky převede na číslo