Domain & CRUD Principles
The core philosophy of DartWay:
➡️ everything starts from domain models
➡️ all Flutter ↔ Server interactions go through CRUD operations
This strict approach ensures consistency, predictability, and AI-friendly development.
DartWay follows the core principles of REST:
- Models behave like REST resources
- Operations map to standard HTTP verbs (
POST,GET,PUT/PATCH,DELETE)
See REST architectural style for more details.
Domain-first mindset
-
Think in models
- Every feature begins with identifying entities.
- Example:
UserProfile,Product,Order,ChatMessage.
-
Relations matter
- Define 1–1, 1–many, many–many relations explicitly.
- Models should reflect the domain reality, not just current UI needs.
-
Single source of truth
- Data lives in models.
- UI is only a projection, never a source of logic.
- No duplicated state outside the generic data layer.
CRUD-only principle
All interactions with models happen via CRUD operations.
👉 In DartWay, Create and Update are unified into a single save operation.
This keeps the API smaller, easier to use, and AI-friendly.
CRUD-only API guarantees:
- Features remain consistent.
- Data layer is unified across Flutter and Server.
- AI assistants can scaffold features reliably.
Flutter (data layer)
saveModeldeleteModelwatchModel/readModel/watchMaybeModel/readMaybeModelwatchModelList/readModelList
read and watch both use the same server-side configuration
(GetModelConfig / GetListConfig).
The only difference is that watch subscribes to updates —
useful for state handling and automatic UI rebuilds.
Safe variants readMaybeModel / watchMaybeModel return null instead of throwing an error if the model is not found.
Server (configs)
SaveConfigDeleteConfigGetModelConfigGetListConfig
On the server, you can define multiple GetModelConfig instances
for the same model with different filters or rules.
Examples:
- One config returns only public profile fields, another includes private fields.
- A config with a
createIfMissingtrigger to automatically create an entity if not found.
How to add advanced logic
Advanced logic should always extend the CRUD layer, not bypass it.
There are three recommended levels of extension:
1. Event Models
Use dedicated models to represent events that change base models.
Example: instead of updating UserProfile.balance directly,
create a BalanceEvent with fields like userId, amount, reason.
Event Models give you:
- Safety → prevent concurrent update conflicts.
- Traceability → every change is logged.
- Consistency → one place for business rules (fees, limits, commissions).
2. CRUD API configuration
Extend CRUD behavior through configs (SaveConfig, DeleteConfig, GetModelConfig):
- Validation → check inputs, enforce domain constraints.
- Pre-processing → enrich or transform data before insert/update.
- Post-processing → run logic after saving (e.g., recalc counters).
- Side effects → trigger notifications, external service calls, etc.
All of these stay within the CRUD layer — no need for separate endpoints.
3. Custom endpoints (last resort)
In rare cases, create a dedicated endpoint.
Use this only when it cannot fit into CRUD patterns.
Examples:
- File upload/download (often still represented as a model like
FileUploadRequest) - External webhooks
- Heavy async processing
Custom endpoints must be clearly documented as exceptions.
Why so strict?
Strict rules may feel limiting at first,
but they pay off with speed, reliability, and safety when the project grows.
- Consistency → everyone knows where to look for logic.
- Maintainability → refactors don’t break hidden actions.
- AI-friendly → code generation always follows the same path.
- Scalability → adding features doesn’t add chaos.
Rule of thumb
- Every new feature = new model(s) + CRUD.
- All Flutter ↔ Server interactions = CRUD operations only.
- Avoid standalone action endpoints unless absolutely unavoidable.