Skip to main content
Version: Next

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 ListDto properties where applicable.
  • Adapt mapping logic for fields that differ between Entity, ListDto and FormDto.

Changing your AppService

Update the service layer to support both DTOs:

  • Change the base class from CrudAppServiceBase to CrudAppServiceListAndItemBase.
    • This base takes additional generics: FormDto, ListDto, Entity, TKey, FilterType, FormMapper, ListMapper.
  • Update the service interface to extend ICrudAppServiceListAndItemBase instead of ICrudAppServiceBase.
    • 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 useCalcMode on the list CrudConfig.
  • Ensure the module routing uses featureListCRUDConfiguration for index/table routes and featureFormCRUDConfiguration for 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 ListDto for the EntityState and adapter (createEntityAdapter<ListDto>()).
  • Keep currentItem and form-related state as FormDto.
  • loadAllByPostSuccess (list-loading success) should carry ListDto items; actions that set or update the current item should use FormDto.
  • Effects that fetch lists should map to ListDto; effects that load single items for edit/read should map to FormDto.

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 FormDto in item, read, edit and new components and their CrudConfig.

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.