Skip to main content

CRUD Configs

In DartWay, all server logic must go through CRUD configs.
This ensures consistency, predictability, and AI-friendly code generation.

πŸ‘‰ Unlike raw Serverpod, DartWay uses wrapper classes (DwSaveConfig, DwDeleteConfig, DwGetModelConfig, DwGetListConfig) where you provide callbacks for validation, processing, and side effects.
πŸ‘‰ All configs for a model are collected into a single DwCrudConfig<T>.


πŸ“¦ DwCrudConfig (entry point per model)​

Each model must have a single DwCrudConfig<T> that aggregates all related configs.
This is the entry point for AI and developers β€” all rules for a model are defined here.

final userProfileCrudConfig = DwCrudConfig<UserProfile>(
table: UserProfile.t,
getModelConfigs: [ ... ],
getListConfig: ...,
saveConfig: ...,
deleteConfig: ...,
);
  • table β†’ required reference to the table.
  • getModelConfigs β†’ may contain multiple configs for different filters/rules.
  • getListConfig β†’ rules for lists.
  • saveConfig β†’ rules for insert/update.
  • deleteConfig β†’ rules for deletion.

πŸ‘‰ If a config is missing, the API will return DwApiResponse.notConfigured.


🟦 DwGetModelConfig​

Purpose: fetch a single model instance by filter.
Allows multiple configs for the same model to support different access rules or projections.

Parameters​

  • filterPrototype β†’ defines which requests this config applies to. It’s used to match incoming query filters with the correct config.
  • include β†’ optional Include object for eager loading relations.
  • additionalEntitiesFetchFunction β†’ function (session, model) => [relatedModels] to include extra related data in the response.
  • createIfMissing β†’ function (session, filter) => model that creates an entity if not found.

Behavior​

  1. Matches request against filterPrototype to decide if config applies.
  2. Fetches entity by filter.
  3. If not found β†’ optionally calls createIfMissing.
  4. Returns wrapped response with the found (or created) entity and additional related models.

Example​

final userProfileGetConfig = DwGetModelConfig<UserProfile>(
filterPrototype: DwBackendFilter(field: 'id'),
include: UserProfile.include(
address: Address.include(),
),
additionalEntitiesFetchFunction: (session, model) async {
return await session.db.find<BalanceEvent>(
where: (t) => t.userProfileId.equals(model.id),
);
},
createIfMissing: (session, filter) async {
return UserProfile(id: filter.value as int, name: 'Guest User');
},
);

🟨 DwGetListConfig​

Purpose: fetch a list of models with optional filters, ordering, pagination, and related data.

Parameters​

  • include β†’ optional Include object for eager loading relations.
  • additionalModelsFetchFunction β†’ function (session, models) => [relatedModels] to include extra related data for the returned list.
  • defaultOrderByList β†’ default ordering if no explicit order is provided in the query.

Behavior​

  1. Fetches models from DB with whereClause, limit, offset.
  2. Orders results with defaultOrderByList if provided.
  3. Wraps results in DwModelWrapper.
  4. Optionally fetches additional related models via additionalModelsFetchFunction.

Example​

final userProfileListConfig = DwGetListConfig<UserProfile>(
include: UserProfile.include(
address: Address.include(),
),
defaultOrderByList: [UserProfile.t.createdAt.desc()],
additionalModelsFetchFunction: (session, models) async {
return await session.db.find<BalanceEvent>(
where: (t) => t.userProfileId.inSet(models.map((u) => u.id)),
);
},
);

🟩 DwSaveConfig​

Purpose: handle creation and update of models with permissions, validation, pre/post-processing, and side effects.

Parameters​

  • allowSave β†’ required permission check for both insert & update.
  • validateSave β†’ optional validation hook, returns error string or null.
  • beforeSave β†’ optional pre-processing inside transaction, can modify model and return extra updates.
  • afterSave β†’ optional post-processing inside transaction, can return additional updated models.
  • afterSaveSideEffects β†’ optional async side effects outside transaction (notifications, background tasks).

Behavior​

  1. Loads initial model (if update).
  2. Runs allowSave check β†’ must return true.
  3. Runs validateSave hook β†’ may return error.
  4. Runs inside transaction:
    • Calls beforeSave (optional) β†’ modifies model & collects updates.
    • Inserts or updates the row.
    • Calls afterSave (optional) β†’ collects extra updates.
  5. Runs afterSaveSideEffects outside transaction (fire-and-forget).
  6. Returns DwApiResponse with saved model and all updated models wrapped.

Example​

final userProfileSaveConfig = DwSaveConfig<UserProfile>(
allowSave: (session, initial, updated) async {
// Only allow if email is unique
final exists = await session.db.findFirstRow<UserProfile>(
where: (t) => t.email.equals(updated.email) & t.id.notEquals(updated.id),
);
return exists == null;
},
validateSave: (session, initial, updated) async {
if (updated.email.isEmpty) return 'Email cannot be empty';
return null;
},
beforeSave: (session, transaction, initial, updated) async {
// Normalize name before save
final processed = updated.copyWith(
name: updated.name.trim(),
);
return DwPreSaveResult(model: processed);
},
afterSave: (session, transaction, initial, saved, {beforeUpdates}) async {
// Return balance events as extra updates
final events = await session.db.find<BalanceEvent>(
where: (t) => t.userProfileId.equals(saved.id),
);
return events.map((e) => DwModelWrapper(object: e)).toList();
},
afterSaveSideEffects: (session, initial, saved, {beforeUpdates, afterUpdates}) async {
// Fire notification
await AppNotifications.sendProfileUpdated(session, saved);
},
);

πŸŸ₯ DwDeleteConfig​

Purpose: handle safe deletion of models with permission checks and post-delete actions.

Parameters​

  • allowDelete β†’ optional function (session, model) => bool to check if model can be deleted.
  • afterDelete β†’ optional function (session, model) => [relatedModels] to return additional updates after deletion.

Behavior​

  1. Loads model by id.
    • If not found β†’ returns ok=true with warning (already deleted).
  2. If no allowDelete provided β†’ returns notConfigured.
  3. Runs allowDelete β†’ must return true.
  4. Deletes row inside DB.
    • If DatabaseException occurs (e.g., FK constraints) β†’ returns error.
  5. Returns DwApiResponse with DwModelWrapper.deleted and optionally related models from afterDelete.

Example​

final userProfileDeleteConfig = DwDeleteConfig<UserProfile>(
allowDelete: (session, model) async {
// Prevent deletion if user still has balance
return model.balance <= 0;
},
afterDelete: (session, model) async {
// Clean up associated balance events
return await session.db.find<BalanceEvent>(
where: (t) => t.userProfileId.equals(model.id),
);
},
);

🚦 Workflow​

  1. Create or update a model in /models.
  2. Generate code:
    dart run serverpod generate
  3. Create a DwCrudConfig<T> in /crud.
  4. Implement validation, permissions, pre/post-processing.
  5. Add side effects where needed (wrap updates in DwModelWrapper).
  6. Write tests in /test.

⚑ Best Practices​

  • Always use CRUD configs β†’ never put logic directly in endpoints.
  • One DwCrudConfig per model β†’ single entry point for all rules.
  • Keep configs small β†’ validation, checks, and side effects only.
  • Use /domain for pure logic (extensions).
  • Use /app for workflows (session-aware services).
  • Event models for transactional flows.
  • Side effects go into afterSave, afterDelete, or dedicated sideEffects.

βœ… Key Takeaways​

  • CRUD configs (DwCrudConfig, DwGetModelConfig, etc.) are the only allowed place for server logic.
  • Every model must have one DwCrudConfig collecting all rules.
  • Keep them predictable and small.
  • Extend logic in /domain or /app, but always trigger through CRUD.
  • Event models + CRUD configs = safe, traceable business flows.
  • See also: Serverpod Endpoints Docs.