Build complete, production-ready data management interfaces in minutes, not days — using .NET 8 Minimal API, Entity Framework Core, React 18, and a single-source-of-truth model design
When I joined a new project , I was handed a backlog of 40+ data entities that needed full CRUD interfaces for example products, customers, orders, invoices, currencies, countries, categories... the list went on. My first instinct was what any experienced developer would do: scaffold them one by one.
Three entities in, I noticed something uncomfortable. I was writing the same code, again and again, with different field names.
That's when the question hit me: What if the code could write itself?
The Problem Nobody Talks About
CRUD — Create, Read, Update, Delete — is the backbone of virtually every business application. Yet it remains one of the most repetitive tasks in software development. Every new entity typically requires:
- A backend controller or route handler
- A database migration
- A list view with sorting, searching, and pagination
- A form with validation
- TypeScript interfaces shared between layers
Multiply that by 20 entities, and you've consumed weeks of engineering time on work that provides zero competitive advantage. You're not solving a hard problem — you're filling in blanks.
The real cost isn't just time. It's inconsistency. When each entity is hand-crafted, small differences creep in. One form validates on blur, another on submit. One table paginates with 10 rows, another with 50. One delete asks for confirmation, another doesn't. These inconsistencies erode trust in your own product.
The Framework at a Glance
The Rapid CRUD Management Framework is a full-stack, production-ready solution built on:
| Layer | Technology |
|---|---|
| Frontend | React 18 + TypeScript + Vite |
| State & Caching | TanStack Query v5 |
| Forms & Validation | react-hook-form + Zod |
| Styling | Tailwind CSS |
| Backend | .NET 8 Minimal API |
| ORM | Entity Framework Core 8 |
| Database | PostgreSQL 16 |
| Containerization | Docker Compose |
The core idea is elegant: your C# model class is the single source of truth for everything — the database schema, the API endpoints, the list columns, and the form fields. Define one annotated class, and the entire stack self-configures.
High-Level Architecture
The system is composed of four runtime layers that communicate through a metadata contract.
Data Flow — What Happens When You Open an Entity Page
- React fetches
GET /api/metadata/Product→ receives a JSON field schema -
EntityCrudPagepasses the schema toDynamicListto build table columns - User clicks "Add" →
DynamicFormuses the same schema to render the form - Zod validation schema is built on the fly from the field definitions
- On submit,
POST /api/crud/Productis called with the validated payload -
GenericRepository<Product>persists via EF Core, invalidates the TanStack Query cache - The list refreshes — zero page reload, zero extra code
System Components — HLD Breakdown
Component 1: The Annotated Entity Model (Backend)
Every entity is a plain C# class that inherits from BaseEntity. Custom attributes decorate each property to describe how it should behave across the entire stack.
public class Product : BaseEntity
{
[DisplayInfo(Label = "Product Name", Placeholder = "Enter name", Order = 1)]
[Required, MaxLength(200), Searchable]
public string Name { get; set; } = string.Empty;
[DisplayInfo(Label = "Price", Order = 2)]
[FieldType(Type = "currency")]
[Range(0.01, 9999999)]
public decimal Price { get; set; }
[DisplayInfo(Label = "Status", Order = 3)]
[FieldType(Type = "select")]
[LookupSource(Options = new[] { "Active", "Discontinued", "Preorder" })]
public string Status { get; set; } = "Active";
[DisplayInfo(Label = "Description", Order = 4)]
[FieldType(Type = "textarea"), HiddenInList]
public string? Description { get; set; }
}
BaseEntity automatically provides Id (UUID), CreatedAt, UpdatedAt, and IsDeleted — so every entity has soft delete and audit timestamps without any additional code.
The available attribute toolkit covers the full spectrum of CRUD needs:
| Attribute | What It Controls |
|---|---|
[DisplayInfo] |
Label, placeholder, ordering, form section grouping |
[FieldType] |
Input type: text, currency, date, select, textarea, checkbox |
[LookupSource] |
Static dropdown options or reference to another entity |
[Searchable] |
Includes field in the global search query |
[HiddenInList] |
Hides field from the table view, keeps it in the form |
[ReadOnly] |
Displayed but not editable after creation |
[Unique] |
Backend validates uniqueness before save |
Component 2: The Reflection Engine & Metadata API (Backend)
At startup, the framework scans all classes that extend BaseEntity using .NET reflection. For each entity it builds a FieldSchema[] — a serializable description of every property including type, validation rules, and display hints.
This schema is exposed at GET /api/metadata/{entityName} and consumed by the React frontend.
GET /api/metadata/Product
→ [
{ "name": "Name", "label": "Product Name", "type": "text", "required": true, "searchable": true },
{ "name": "Price", "label": "Price", "type": "currency", "required": true, "min": 0.01 },
{ "name": "Status", "label": "Status", "type": "select", "lookup": { "options": ["Active","Discontinued","Preorder"] } },
{ "name": "Description", "label": "Description", "type": "textarea", "hiddenInList": true }
]
The EntityTypeRegistry auto-discovers all entities — no manual registration, no factory switch-case. Add a class, restart, it appears. The generic CRUD endpoints resolve entity names to CLR types at runtime and delegate to a single GenericRepository<T> that handles paged queries, search, sort, and soft delete for any entity.
Component 3: Generic CRUD Endpoints (Backend)
A single set of Minimal API routes serves every entity. Route resolution happens at runtime, not compile time.
GET /api/crud/{entity} → paginated list with search + sort
GET /api/crud/{entity}/{id} → single record
POST /api/crud/{entity} → create
PUT /api/crud/{entity}/{id} → partial update
DELETE /api/crud/{entity}/{id} → soft delete (IsDeleted = true)
GET /api/crud/{entity}/lookup → dropdown options for relation fields
The GenericRepository<T> applies searchable fields dynamically via expression trees, so search across Name, SKU, and Email simultaneously — without writing a single LINQ query per entity.
Component 4: Schema-Driven React Components (Frontend)
The React side has no entity-specific code at all. The framework layer consists of four composable components that together produce a complete CRUD page:
EntityCrudPage ← top-level orchestrator (state, routing, mutations)
├── ListToolbar ← search input + "Add" button
├── DynamicList ← table columns generated from FieldSchema[]
│ └── columns built via TanStack Table column helpers
└── DynamicForm ← modal form with inputs generated from FieldSchema[]
└── DynamicField ← per-field renderer (switch on field.type)
DynamicField — The Heart of the UI Engine
The DynamicField component is where field type meets input element. A simple type switch renders the correct control — and the whole component is 80 lines:
// Simplified illustration of the field type dispatch
switch (field.type) {
case 'textarea':
return <textarea {...register(field.name)} />;
case 'select':
return (
<select {...register(field.name)}>
{(field.lookup?.options ?? lookupOptions).map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
);
case 'checkbox':
return <input type="checkbox" {...register(field.name)} />;
case 'currency':
return <input type="number" step="0.01" {...register(field.name, { valueAsNumber: true })} />;
default:
return <input type="text" {...register(field.name)} />;
}
Runtime Zod Schema Generation
One of the more elegant pieces is the Zod schema builder. Rather than writing validation schemas by hand, the framework derives them from the same FieldSchema[] that drives the UI:
// For each field in the metadata, a Zod rule is constructed at runtime
for (const field of fields) {
let rule = field.type === 'number' || field.type === 'currency'
? z.number()
: z.string();
if (field.required) rule = rule.min(1, `${field.label} is required`);
if (field.maxLength) rule = (rule as z.ZodString).max(field.maxLength);
if (field.min != null) rule = (rule as z.ZodNumber).min(field.min);
shape[field.name] = field.required ? rule : rule.optional();
}
return z.object(shape);
This means validation rules are always in sync with your C# model. Add [MaxLength(50)] to a backend property and the frontend Zod schema updates automatically on next schema fetch — no frontend change required.
Component 5: Auto-Generated Navigation (Frontend)
The sidebar navigation is fully dynamic. It calls GET /api/metadata which returns the list of all registered entities. The sidebar renders a NavLink for each one — no routes to declare, no sidebar items to maintain.
// Sidebar populates itself from the metadata API
const { data: entities } = useQuery({
queryKey: ['entities'],
queryFn: () => crudApi.getEntities(),
staleTime: Infinity,
});
// → entities = [{ name: "Product" }, { name: "Customer" }, { name: "Order" }, ...]
Add a new C# entity class, restart the API, and it appears in the sidebar. No frontend deployment needed.
Component 6: Infrastructure & Deployment
The entire stack runs with a single command via Docker Compose:
docker compose up -d
Three services start in dependency order:
db (PostgreSQL 16-alpine)
└─► api (.NET 8 — auto-migrates schema on startup)
└─► frontend (React SPA served by Nginx)
The .NET API applies EF Core migrations automatically at boot. There is no manual database setup step. The Nginx frontend config proxies /api/ requests to the backend, so the React app has no CORS concerns in either development or production.
The Developer Experience in Numbers
| Task | Traditional (per entity) | This Framework |
|---|---|---|
| Backend controller | ~60 lines | 0 lines |
| Repository/service | ~80 lines | 0 lines |
| TypeScript interfaces | ~20 lines | 0 lines |
| React list page | ~100 lines | 0 lines |
| React form | ~80 lines | 0 lines |
| Validation schema | ~30 lines | 0 lines |
| API wiring | ~40 lines | 0 lines |
| Total per entity | ~410 lines | ~40 lines (model only) |
| Time to implement | 3–6 hours | < 10 minutes |
Benefits at a Glance
Radical Consistency
Every entity goes through the same pipeline. Every list looks the same, every form behaves the same, every API follows the same conventions. Search, sort, pagination, validation — all consistent, all automatic.
Single Source of Truth
Your C# model is your schema, your API contract, your UI specification, and your validation rules — all at once. Rename a field, change a max length, add a dropdown — it propagates to the entire stack without touching the frontend.
Built-in Soft Delete & Audit Readiness
Every entity automatically gets CreatedAt, UpdatedAt, and IsDeleted. A global EF Core query filter ensures deleted records never appear in queries. No accidental data loss. Natural audit trail from day one.
Near-Zero Onboarding Cost
A new developer doesn't need to learn the UI conventions, the API patterns, or the validation approach separately. They learn to annotate a model class, and everything else follows.
Small, Tested Surface Area
The framework layer is written once and tested once. Adding 30 more entities doesn't add 30 more code paths to maintain or break.
Potential Use Cases
Internal Admin Tools & Back-Office Systems
Operations teams managing lookup tables, configuration values, approval lists, and reference data. Zero UI development overhead.
ERP-Style Applications
Dozens of related entities — products, customers, orders, invoices, warehouses, currencies — all generated consistently from model classes.
Data Management Portals
Domain experts need structured data management. The framework provides a polished, consistent interface without a dedicated frontend team per entity.
MVPs and Rapid Prototyping
Validate a product idea fast. Define your domain model, run the stack, and focus engineering effort on the differentiating features.
Microservice Admin Interfaces
Each microservice often needs a lightweight admin interface for its own data. A shared framework keeps these consistent and cost-effective to build.
Configuration & Feature Management Systems
Feature flags, environment configs, access control lists, country/currency registries — all manageable via the same framework with one model class each.
Adoption Path
The framework is designed to be adopted incrementally. You don't need to commit fully upfront.
Phase 1 — Evaluate (Day 1)
docker compose up and explore the running application with six pre-built entities. Read the model classes. Understand the pattern.
Phase 2 — First Entity (Day 1–2)
Pick the simplest entity in your backlog. Define the model class, run the EF migration, add one line to the entity registry. Verify it works end-to-end.
Phase 3 — Migrate Existing Entities (Week 1–2)
Identify pure CRUD entities in your existing codebase. Migrate them to the framework. Keep complex workflow-driven entities hand-crafted.
Phase 4 — Extend (Ongoing)
Add a custom field type, a special validator, a calculated column — extend the framework rather than bypass it. Every extension benefits all future entities.
Design Principles
Convention over configuration. Sensible defaults mean you only specify what differs. Most fields work perfectly with no attributes beyond [DisplayInfo].
The model is the spec. No separate JSON schemas, no configuration files, no API documentation that drifts out of sync with reality. The code is the contract.
Double validation. Zod runs on the frontend before the request is sent. Data Annotations run on the backend before the database is touched. Invalid data cannot reach storage through any path.
Extensibility without modification. New field types, custom renderers, and specialized validators slot into the framework without touching existing code. Clear seams, no monkey-patching.
What the Framework Doesn't Do
This approach is optimized for standard data management scenarios. It intentionally steps aside for:
- Complex workflows — multi-step wizards, approval chains, and stateful processes need custom UI
- Rich relational views — deeply nested or joined data across many entities requires custom components
- Domain-specific business logic — cross-entity validation, calculated fields, and conditional rules belong in your application layer
The framework handles the 80% of work that is genuinely identical across entities, so your team can spend its energy on the 20% that genuinely isn't.
The Bigger Picture
Repetition in code is a signal, not a fact of life. When you find yourself writing the same patterns for the 10th time, the right response isn't to write it faster — it's to write it once and derive the rest.
Metadata-driven systems have existed in enterprise software for decades. What's changed is that modern tooling — TypeScript's type inference, React's composability, .NET's reflection APIs, EF Core's fluent modeling — makes them dramatically more approachable than they once were.
The framework described in this article took roughly 2,300 lines to build — split across ~1,500 lines of backend infrastructure and ~800 lines of React framework components. It has paid for itself many times over. More importantly, it means the next 30 entities in any backlog represent a morning's work, not a month's.
Getting Started
git clone <repo>
cd metadata-crud
docker compose up -d
# Access at:
# Frontend: http://localhost:3000
# API: http://localhost:5000
# Swagger: http://localhost:5000/swagger
Six entities are pre-configured and immediately usable. The repository includes a step-by-step guide for adding your first custom entity, a complete API reference, and troubleshooting notes for common scenarios.
Final Thought
The best code is often the code you didn't have to write. A metadata-driven framework doesn't make you a less skilled developer — it makes you a more effective one. It channels your skill toward the problems that actually need it, and automates away the ones that don't.
If you've ever sighed before opening a new blank controller file, this might be worth an afternoon of your time.

Top comments (0)