admin-list-pattern
Admin List Integration Pattern
This skill outlines the standard pattern for implementing searchable, paginated lists in the erify_studios (frontend) and erify_api (backend) applications.
Canonical Examples
Study these real implementations:
- Backend: admin-client.controller.ts
- Repository: client.repository.ts
Integration Overview
The pattern relies on synchronized parameter names and behaviors across the stack:
- Frontend: Uses
useTableUrlStateto sync URL params (e.g.,?name=...) with the table'scolumnFilters. - API Boundary: A specialized
List<Resource>QueryDtoextends the base pagination schema. - Repository: Builds a Prisma
whereclause to handle partial matches and other filters. The Service is a thin pass-through.
Backend Pattern (erify_api)
1. Define the Query DTO (schemas.ts)
Nest the filters inside a Zod schema and extend the base pagination. Following the pattern in models/client/schemas/client.schema.ts:
export const listResourceFilterSchema = z.object({
name: z.string().optional(),
include_deleted: z.coerce.boolean().default(false),
});
export const listResourceQuerySchema = z
.object({
page: z.coerce.number().int().min(1).optional().default(1),
limit: z.coerce.number().int().min(1).optional().default(10),
})
.and(listResourceFilterSchema)
.transform((data) => ({
...data,
take: data.limit,
skip: (data.page - 1) * data.limit,
}));
export class ListResourceQueryDto extends createZodDto(listResourceQuerySchema) {}
2. Repository Logic (repository.ts)
Build the where clause in the repository. Ensure case-insensitive partial matching for strings.
async findPaginated(params: {
skip?: number;
take?: number;
name?: string;
includeDeleted?: boolean;
}): Promise<{ data: Resource[]; total: number }> {
const where: Prisma.ResourceWhereInput = {};
if (!params.includeDeleted) {
where.deletedAt = null;
}
if (params.name) {
where.name = {
contains: params.name,
mode: 'insensitive',
};
}
const [data, total] = await Promise.all([
this.model.findMany({ skip: params.skip, take: params.take, where }),
this.model.count({ where }),
]);
return { data, total };
}
3. Service Logic (service.ts)
Service passes parameters to repository without building Prisma where clauses.
async getResources(
...args: Parameters<ResourceRepository['findPaginated']>
): Promise<{ data: Resource[]; total: number }> {
return this.repository.findPaginated(...args);
}
4. Controller Integration (controller.ts)
Pass the query DTO to the service and use @AdminPaginatedResponse.
@Get()
@AdminPaginatedResponse(ResourceDto, 'List resources')
async getResources(@Query() query: ListResourceQueryDto) {
const { data, total } = await this.service.getResources(query);
return this.createPaginatedResponse(data, total, query);
}
Frontend Pattern (erify_studios)
1. Route Search Schema
Ensure the Route search schema includes the filter field. Always use limit (not pageSize) as the URL param name.
const searchSchema = z.object({
page: z.coerce.number().int().min(1).catch(1),
limit: z.coerce.number().int().min(10).max(100).catch(10),
name: z.string().optional().catch(undefined),
});
limitvspageSize:limitis the URL param used in route schemas and navigation objects. TanStack Table'sPaginationStatetype usespageSizeinternally — this appears aspagination.pageSizein feature hooks andpaginationState={{ pageSize }}inDataTableprops. Do not rename those:useTableUrlStatebridges the two by readinglimitfrom the URL and returning TanStack'sPaginationState. Seetable-view-patternfor the full breakdown.
2. DataTable Configuration
Pass searchColumn and onColumnFiltersChange to DataTable via DataTableToolbar.
const {
pagination,
onPaginationChange,
columnFilters,
onColumnFiltersChange
} = useTableUrlState({ from: '/system/resources/' });
const nameFilter = columnFilters.find(f => f.id === 'name')?.value as string;
const { data, isLoading } = useAdminList<Resource>('resources', {
page: pagination.pageIndex + 1,
limit: pagination.pageSize,
name: nameFilter,
});
// ... inside render
<DataTable
// ...
columnFilters={columnFilters}
onColumnFiltersChange={adaptColumnFiltersChange(columnFilters, onColumnFiltersChange)}
renderToolbar={(table) => (
<DataTableToolbar
table={table}
searchColumn="name"
searchableColumns={resourceSearchableColumns}
/>
)}
/>
3. Toolbar UX (Debouncing)
The DataTableToolbar (generic component) should handle internal debouncing of the input to avoid immediate server queries on every keystroke.
- Timeout: Use a 500ms debounce.
- Visibility: Only show the search input when
searchColumnis provided.
Checklist
- Backend:
QueryDtoextends pagination and includes filters. - Backend: Repository builds
whereclause withcontainsandinsensitive(NOT the service). - Backend: Service delegates directly to
repository.findPaginated()usingParameters<>spread. - Frontend:
useTableUrlStateused for URL synchronization. - Frontend:
searchColumnpassed toDataTableToolbar. - Frontend: Verification of debounced input behavior.
More from allenlin90/eridu-services
service-pattern-nestjs
Comprehensive NestJS service implementation patterns. This skill should be used when implementing Model Services, Orchestration Services, or business logic with NestJS decorators.
8erify-authorization
Patterns for implementing authorization in erify_api with current StudioMembership + AdminGuard behavior, plus planned RBAC references
6data-validation
Provides comprehensive guidance for input validation, data serialization, and ID management in backend APIs. This skill should be used when designing validation schemas, transforming request/response data, mapping database IDs to external identifiers, and ensuring type safety across API boundaries.
6code-quality
Provides general code quality and best practices guidance applicable across languages and frameworks. Focuses on linting, testing, and type safety.
6repository-pattern-nestjs
Comprehensive Prisma repository implementation patterns for NestJS. This skill should be used when implementing repositories that extend BaseRepository or use Prisma delegates.
6task-template-builder
Provides guidelines for the Task Template Builder architecture, including Schema alignment, Draft storage, Drag-and-Drop, and Validation logic.
6