Using different DTOs for List and Form
This guide explains how and why to use separate DTOs for list (table) views and for form (create/edit) views, and lists the concrete changes required on both backend and frontend.
Why do this?
- List DTOs are optimized for display and performance (smaller payloads, formatted fields, aggregated values).
- Form DTOs are optimized for editing (complete/nested data, shape suited to validation and binding).
- Separating the two reduces coupling between table and form concerns, improves list load performance, and keeps mapping and validation logic explicit and testable.
Backend
Creating a second DTO
Create two DTO types: one optimized for list display and one for forms. You can reuse your existing DTO as either ListDto or FormDto and add the second one.
Guidance:
ListDto: include only fields required for the table (IDs, summary fields, pre-formatted values). Keep payloads small for performance.FormDto: include all fields required for create/edit operations, nested structures and any validation-related shapes.
Creating a second mapper
Add a mapper for the new DTO. The existing mapper can be duplicated and adapted.
Checklist when adapting the mapper:
- Update generic types (e.g.
DtoToEntity,EntityToDto,DtoToCellMapping,MapEntityKeysInDto). - Adjust header/column names to match
ListDtoproperties where applicable. - Adapt mapping logic for fields that differ between
Entity,ListDtoandFormDto.
Changing your AppService
Update the service layer to support both DTOs:
- Change the base class from
CrudAppServiceBasetoCrudAppServiceListAndItemBase.- This base takes additional generics:
FormDto,ListDto,Entity,TKey,FilterType,FormMapper,ListMapper.
- This base takes additional generics:
- Update the service interface to extend
ICrudAppServiceListAndItemBaseinstead ofICrudAppServiceBase.- Adjust generic type parameters accordingly.
Frontend
Creating a second DTO
Create corresponding DTOs in the frontend models and separate CrudConfig instances for list and form views. Each CrudConfig defines the fieldsConfig for its DTO.
Example:
export const featureListCRUDConfiguration: CrudConfig<ListDto> = new CrudConfig({
...
fieldsConfig: featureListFieldsConfiguration,
...
})
export const featureFormCRUDConfiguration: CrudConfig<FormDto> = new CrudConfig({
...
fieldsConfig: featureFormFieldsConfiguration,
...
})
Notes:
- Disable
useCalcModeon the listCrudConfig. - Ensure the module routing uses
featureListCRUDConfigurationfor index/table routes andfeatureFormCRUDConfigurationfor read/edit/new routes. After that, check that your feature.module routing correctly uses the featureListCRUDConfiguration and not the featureFormCRUDConfiguration for routing behavior.
Services
Update service classes to include both DTO types in their generics.
export class FeatureService extends CrudItemService<ListDto, FormDto>
export class FeatureDas extends AbstractDas<ListDto, FormDto>
export class FeatureService extends CrudItemService<ListDto, FormDto>
export class FeatureDas extends AbstractDas<ListDto, FormDto>
Store
State and reducer changes:
export interface State extends CrudState<FormDto>, EntityState<ListDto> {}
Checklist:
- Use
ListDtofor theEntityStateand adapter (createEntityAdapter<ListDto>()). - Keep
currentItemand form-related state asFormDto. loadAllByPostSuccess(list-loading success) should carryListDtoitems; actions that set or update the current item should useFormDto.- Effects that fetch lists should map to
ListDto; effects that load single items for edit/read should map toFormDto.
Components
Component simple changes:
export class FeaturesIndexComponent extends CrudItemsIndexComponent<
ListDto,
FormDto
> {}
export class FeaturesTableComponent extends CrudItemTableComponent<
ListDto,
FormDto
> {}
Checklist:
- Ensure index/table components use the list
CrudConfig. - Use
FormDtoin item, read, edit and new components and theirCrudConfig.
Mass Import feature
Mass import feature need more modifications to work because a form element can now be very different from the table elements. A provider for the injection token SAME_LIST_FORM_MODELS must be added to notify the CrudItemImportService that it will work with different objects than the table.
@Component({
...
providers: [
CrudItemImportService,
{ provide: SAME_LIST_FORM_MODELS, useValue: false },
],
})
export class FeatureImportComponent extends CrudItemImportComponent<
ListDto,
FormDto
> {}
This modification will change the endpoint to get the list of elements to compare to the CSV file elements. You need to create this new endpoint in your controller:
/// <summary>
/// Get all feature items with filters.
/// </summary>
/// <param name="filters">The filters.</param>
/// <returns>The list of feature items.</returns>
[HttpPost("allItems")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[Authorize(Roles = nameof(PermissionId.Feature_List_Access))]
public async Task<IActionResult> GetAllItems([FromBody] PagingFilterFormatDto filters)
{
var (results, total) = await this.featureService.GetRangeItemsAsync(filters);
this.HttpContext.Response.Headers.Append(BiaConstants.HttpHeaders.TotalCount, total.ToString());
return this.Ok(results);
}
PagingFilterFormatDto could be another type depending on your feature filter model.