A hands-on learning project using Minimal APIs, Entity Framework Core, and a clean layered architecture
Repository name: restful_api_training
- Project Overview
- Current Status
- Learning Goals
- Domain Model — Users
- High-Level Architecture
- Solution & Folder Structure
- Technology Stack
- REST API Design
- DTOs & Request/Response Models
- Database Schema
- REST Constraints & How This Project Demonstrates Them
- Non-Functional Requirements (NFRs)
- Running the Project (Local Dev)
- Recommended Training Flow
- Future Extensions
This repository contains a training-oriented implementation of a modern RESTful API using:
- .NET 10 Minimal APIs
- MySQL as the backing relational database
- Entity Framework Core for data access
- A clean, layered architecture that can scale to enterprise systems with hundreds of tables
The goal is to keep the functionality small (single resource: users) while designing the architecture like a real enterprise application.
This allows learners to see how a simple CRUD demo fits into a much larger, production-ready design.
The main resource in this training project is the users table with:
- Full CRUD operations
- A generic profile picture upload endpoint (file stored on disk, URL stored in DB)
A separate document, RESTful.md, is included in the repo to teach the theory of REST, RPC, SOAP, etc.
This README.md focuses on the code, architecture, and practical walkthrough.
As of now, the project implements:
- ✅ Layered solution: Domain, Application, Infrastructure, API
- ✅ EF Core integration with MySQL (via Pomelo provider)
- ✅ Database migrations (
InitialCreate) anduserstable - ✅ Seeded
adminuser created directly via DB for testing - ✅ Minimal APIs for:
- Create user
- Read user by ID
- List users (with pagination)
- List users (all)
- Filtered listing (with/without pagination)
- Update user
- Delete user
- Upload profile picture (generic field & folder)
- ✅ Swagger UI + OpenAPI JSON
- ✅ Simple HTML landing page at
/with links
Planned but not implemented yet (for future training sessions):
- ❌ JWT authentication
- ❌ Role-based authorization (
adminvsread_only) - ❌ Auth endpoints (
/api/auth/login) - ❌ Centralised error middleware, ProblemDetails, etc.
For tomorrow’s session, the focus is on:
- The architecture
- The Minimal API endpoints
- The EF Core + MySQL integration
- The RESTful design (URLs, verbs, status codes)
By working through this project, learners should be able to:
- Understand the basics of RESTful API design
(resources, verbs, status codes, uniform interface) - See how Minimal APIs can replace controllers while remaining organized and testable
- Learn how to structure a project using Domain → Application → Infrastructure → API layers
- Integrate MySQL with EF Core in a clean, maintainable way
- Implement pagination and filtering for list endpoints
- Implement a generic file upload endpoint and update the corresponding user column
- Explain how the implementation satisfies the REST constraints during training
- Recognize how this design can evolve to include:
- JWT authentication & authorization
- Additional bounded contexts and entities
The demo uses a single domain entity: User.
Table name: users
| Column | Type | Constraints | Notes |
|---|---|---|---|
id |
BIGINT | PK, Auto Increment | Unique identifier |
user_name |
VARCHAR(100) | NOT NULL, UNIQUE | Used for login |
password_hash |
VARCHAR(255) | NOT NULL | Securely stored password hash |
full_name |
VARCHAR(200) | NOT NULL | Display name |
role |
ENUM | NOT NULL (Admin, read_only) |
Access level / permissions |
email_id |
VARCHAR(255) | NOT NULL | Email address |
mobile_num |
VARCHAR(20) | NOT NULL | Mobile number |
profile_pic_url |
VARCHAR(500) | NULL | URL/path to profile picture file |
created_at |
DATETIME | NOT NULL, default current timestamp | Row creation timestamp |
Important for training:
- The input model exposes
password, but the database stores a hash (password_hash).- This is a good teaching moment to explain why plain-text passwords are unsafe.
The project follows a layered / clean architecture approach, typical for enterprise systems that may eventually grow to hundreds of tables and multiple bounded contexts.
[ Client ] (Postman, Browser, Web SPA, Mobile App)
|
v
+-------------------------------------------+
| Users.Api (Minimal APIs) |
| - HTTP endpoints (UsersEndpoints) |
| - Routing, input/output models |
| - Swagger / OpenAPI |
| - Basic landing page (/) |
+-------------------------------------------+
|
v
+-------------------------------------------+
| Users.Application |
| - Use cases / services (UserService) |
| - DTOs (CreateUserRequest, UserDto, etc.) |
| - Contracts (IUserRepository) |
+-------------------------------------------+
|
v
+-------------------------------------------+
| Users.Domain |
| - Entities (User) |
| - Enums (UserRole) |
| - Domain rules |
+-------------------------------------------+
|
v
+-------------------------------------------+
| Users.Infrastructure |
| - EF Core + MySQL |
| - DbContext, entity configurations |
| - Migrations (InitialCreate) |
| - Repository implementations |
+-------------------------------------------+
|
v
[ MySQL Database ]
The API layer is intentionally thin.
Business rules, mapping, and persistence concerns live in Application, Domain, and Infrastructure.
Actual layout under the repository root:
restful_api_training/
├── LICENSE
├── README.md # This file – architecture & code overview
├── RESTful.md # Theory: RPC → SOAP → REST → etc.
├── restful_api_training.slnx
└── src
└── Users
├── Users.Api/ # HTTP endpoints, Swagger, startup
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ ├── Endpoints/
│ │ └── UsersEndpoints.cs # Minimal API route mappings for /api/users
│ ├── Program.cs # Composition root, DI, Swagger, OpenAPI
│ ├── Properties/
│ │ └── launchSettings.json
│ ├── uploads/
│ │ └── profilepic/ # Uploaded files (runtime, usually gitignored)
│ ├── Users.Api.csproj
│ └── Users.Api.http # HTTP file for quick API testing (VS Code)
│
├── Users.Application/ # Use cases, DTOs, interfaces
│ ├── Contracts/
│ │ └── IUserRepository.cs # Abstraction for user persistence
│ ├── DependencyInjection/
│ │ └── ApplicationServiceCollectionExtensions.cs
│ ├── Users/
│ │ ├── Dtos/
│ │ │ ├── CreateUserRequest.cs
│ │ │ ├── UpdateUserRequest.cs
│ │ │ ├── UserDto.cs
│ │ │ └── UserFilterRequest.cs
│ │ └── Services/
│ │ └── UserService.cs # Business logic around users
│ └── Users.Application.csproj
│
├── Users.Domain/ # Core business model
│ ├── Entities/
│ │ └── User.cs
│ ├── Enums/
│ │ └── UserRole.cs
│ └── Users.Domain.csproj
│
└── Users.Infrastructure/ # EF Core + MySQL, repositories, DI
├── DependencyInjection/
│ └── InfrastructureServiceCollectionExtensions.cs
├── Persistence/
│ ├── Configurations/
│ │ └── UserConfiguration.cs
│ ├── Migrations/
│ │ ├── 20251208143050_InitialCreate.cs
│ │ ├── 20251208143050_InitialCreate.Designer.cs
│ │ └── UsersDbContextModelSnapshot.cs
│ ├── UsersDbContext.cs
│ └── UsersDbContextFactory.cs
├── Repositories/
│ └── UserRepository.cs
└── Users.Infrastructure.csproj
During training, you can explicitly connect each folder to its responsibility, and show how this structure can be replicated for other bounded contexts (e.g.,
Tickets,Orders,Billing, etc.).
-
Runtime / Framework
- .NET 10
- ASP.NET Core Minimal APIs (no MVC controllers)
-
Language
- C# (latest available with .NET 10)
-
Database
- MySQL 8+ (e.g., running in Docker)
-
Data Access
- Entity Framework Core
- MySQL provider: Pomelo.EntityFrameworkCore.MySql
-
Tooling
- Swagger / OpenAPI for interactive API docs
appsettings.json+ environment variables for configuration- Postman or HTTP file (
Users.Api.http) for testing
-
Planned (not yet implemented)
- JWT Bearer Authentication
- Role-based Authorization (
Admin,read_only) - Dedicated auth endpoints
Base route for the resource: /api/users
All routes are implemented using Minimal APIs with an endpoint group in UsersEndpoints.cs.
Note: Authentication & authorization are not enabled yet.
For now, all endpoints are open for training/demo purposes.
| # | Operation | Method | URL |
|---|---|---|---|
| 1 | Create user | POST | /api/users |
| 2 | Get user by ID | GET | /api/users/id/{id} |
| 3 | Get all users (paged) | GET | /api/users?skip={x}&take={y} |
| 4 | Get all users (no paging) | GET | /api/users/all |
| 5 | Filtered users (paged) | GET | /api/users/filter?...&skip=&take= |
| 6 | Filtered users (no paging) | GET | /api/users/filter/all?... |
| 7 | Update user | PUT | /api/users/id/{id} |
| 8 | Upload profile picture (generic) | POST | /api/users/id/{id}/upload |
| 9 | Delete user | DELETE | /api/users/id/{id} |
Additional non-API endpoints for convenience:
GET /— HTML landing page explaining the project and linking to key URLsGET /swagger— Swagger UIGET /openapi/v1.json— OpenAPI JSON documentGET /weatherforecast— Sample template endpoint (for reference)
GET /api/users/id/{id}is intentionally used instead ofGET /api/users/{id}to:- Avoid future ambiguity if alternate lookup routes are added (e.g.,
/api/users/email/{email}). - Create a clear pattern:
/api/users/{identifier-type}/{value}.
- Avoid future ambiguity if alternate lookup routes are added (e.g.,
- List endpoints follow standard REST practices with query parameters for pagination and filtering.
URL:
POST /api/users/id/{id}/upload
Request format: multipart/form-data
Form-data fields:
| Field | Type | Required | Description |
|---|---|---|---|
file |
binary | Yes | The file to upload |
folder |
string | Yes | Logical folder/group under storage (e.g. profilepic) |
fieldname |
string | Yes | Column/field name to update (e.g. profile_pic_url) |
Behavior:
-
The API stores the uploaded file into:
<content-root>/uploads/{folder}/<generated-file-name>For this project:
src/Users/Users.Api/uploads/profilepic/... -
It computes a relative URL like:
/uploads/{folder}/{fileName} -
It updates the specified column (e.g.,
profile_pic_url) in theuserstable for the givenid. -
It returns the updated
UserDtoJSON for convenience.
This endpoint is designed to be generic so it can support other file-related columns in future.
For now, static file serving of
/uploads/...from the API is a training discussion point and can be enabled withapp.UseStaticFiles().
Common response patterns used in the project:
200 OK— Successful GET / PUT / POST (non-creation) operations201 Created— Successful creation of a new user204 No Content— Successful deletion with no content body400 Bad Request— Validation errors, malformed input, missing data404 Not Found— User not found for givenid409 Conflict— Duplicate username or conflicting constraints (where applicable)500 Internal Server Error— Unhandled errors (currently default behavior)
In a future iteration, you can add:
- A global error handling middleware
- A standard error response shape (e.g.
ProblemDetails)
To separate domain entities from API contracts, the API uses DTOs and request models in the Application layer.
All of the below live under:
src/Users/Users.Application/Users/Dtos/
public sealed class UserDto
{
public long Id { get; init; }
public string UserName { get; init; } = default!;
public string FullName { get; init; } = default!;
public string Role { get; init; } = default!; // "Admin" or "read_only"
public string EmailId { get; init; } = default!;
public string MobileNum { get; init; } = default!;
public string? ProfilePicUrl { get; init; }
public DateTime CreatedAt { get; init; }
}public sealed class CreateUserRequest
{
public string UserName { get; init; } = default!;
public string Password { get; init; } = default!;
public string FullName { get; init; } = default!;
public string Role { get; init; } = "read_only"; // default role
public string EmailId { get; init; } = default!;
public string MobileNum { get; init; } = default!;
}public sealed class UpdateUserRequest
{
public string? FullName { get; init; }
public string? Role { get; init; } // Optional change of role
public string? EmailId { get; init; }
public string? MobileNum { get; init; }
}public sealed class UserFilterRequest
{
public string? UserName { get; init; }
public string? Role { get; init; }
public string? EmailId { get; init; }
public string? MobileNum { get; init; }
public int? Skip { get; init; }
public int? Take { get; init; }
}The Minimal APIs (UsersEndpoints.cs) receive query/body parameters, construct these request objects, and pass them to UserService.
Example conceptual SQL for the users table (EF Core migrations already generate this):
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(200) NOT NULL,
role ENUM('Admin', 'read_only') NOT NULL,
email_id VARCHAR(255) NOT NULL,
mobile_num VARCHAR(20) NOT NULL,
profile_pic_url VARCHAR(500) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);Currently, an initial admin user was inserted directly via MySQL, e.g.:
INSERT INTO users (user_name, password_hash, full_name, role, email_id, mobile_num)
VALUES (
'admin',
'<sha256-hash-of-admin-password>',
'API Admin',
'Admin',
'x@y.com',
'9876543210'
);You can use this user to test GET endpoints and uploads.
-
Client–Server
- The API does not render business UI; it exposes JSON over HTTP.
- Clients (Postman, browser, future SPA, mobile app) are decoupled from server.
-
Stateless
- No session state stored on the server.
- Each request is self-contained.
- Future JWT-based auth will further emphasise statelessness.
-
Cacheable
- All
GETendpoints are safe (do not modify server state). - They can be cached by clients or intermediaries (e.g., via headers in future).
- All
-
Uniform Interface
- Resource-oriented URLs:
/api/users,/api/users/id/{id},/api/users/filter. - Standard HTTP methods:
GETfor read-only operationsPOSTfor creation and uploadPUTfor updatesDELETEfor deletions
- Standard HTTP status codes and JSON payloads.
- Resource-oriented URLs:
-
Layered System
- Clear separation between:
- API (transport & HTTP concerns)
- Application (use cases, orchestration)
- Domain (core model)
- Infrastructure (DB, EF Core, repositories)
- The database, or even EF Core, could be swapped without changing API contracts.
- Clear separation between:
-
Code-on-Demand (optional)
- Not used in this project (typical for JSON APIs).
- Good opportunity to explain that this is an optional REST constraint.
- Statelessness
- No in-memory session; each request is independent.
- Security
- Passwords stored as hashes (
password_hash). - Authentication/authorization will be added later.
- Passwords stored as hashes (
- Correctness
- Explicit DTOs for create/update operations.
- Clear HTTP status codes for common failure cases.
- Performance
- Asynchronous APIs (
async/await) and database calls. - Pagination and filtering supported for user listing.
- Asynchronous APIs (
- Scalability
- Layered design allows scaling out API instances behind a load balancer.
- Easy to extend domain model and add new bounded contexts by cloning the pattern.
- Maintainability
- Clean separation of concerns using multiple projects.
- Dependency Injection for swappable implementations.
- Observability (future)
- Logging via built-in
ILogger. - Later: structured logs, metrics, tracing, etc.
- Logging via built-in
- .NET 10 SDK installed
- MySQL 8+ running (e.g., via Docker)
- VS Code (optional but recommended)
- Postman or any HTTP client (or just use
Users.Api.http)
git clone https://github.com/<your-username>/restful_api_training.git
cd restful_api_trainingCreate a database:
CREATE DATABASE restful_api_training;Make a note of:
- Host:
localhost - Port:
3306 - User: e.g.
root - Password: e.g.
pa55word
Update src/Users/Users.Api/appsettings.Development.json:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Information"
}
},
"ConnectionStrings": {
"Default": "Server=localhost;Port=3306;Database=restful_api_training;User=root;Password=pa55word;TreatTinyAsBoolean=false"
}
}(Adjust user/password as per your local setup.)
From the repo root:
dotnet ef database update --project src/Users/Users.Infrastructure --startup-project src/Users/Users.ApiThis will:
- Create the
userstable (and any future tables) - Apply the
InitialCreatemigration
From the repo root:
dotnet run --project src/Users/Users.ApiYou should see logs similar to:
Now listening on: http://localhost:5117
Application started. Press Ctrl+C to shut down.
Hosting environment: Development
Content root path: .../src/Users/Users.Api
Open a browser:
-
Landing page:
http://localhost:5117/ -
Swagger UI:
http://localhost:5117/swagger -
OpenAPI JSON:
http://localhost:5117/openapi/v1.json -
Example API calls:
GET http://localhost:5117/api/users/allGET http://localhost:5117/api/users?id=...POST http://localhost:5117/api/users(create)PUT http://localhost:5117/api/users/id/{id}(update)DELETE http://localhost:5117/api/users/id/{id}(delete)POST http://localhost:5117/api/users/id/{id}/upload(multipart/form-data)
Suggested sequence for a 60–90 minute training/demonstration:
-
Introduce the domain
- Explain the
usersentity and its fields. - Show the
userstable in MySQL (admin row, etc.).
- Explain the
-
Walk through the architecture diagram
- Explain the layered structure.
- Clarify which project does what (Api, Application, Domain, Infrastructure).
-
Show the folder structure in the repo
- Walk line-by-line through the structure in VS Code.
- Connect each folder to a responsibility: endpoints, services, repository, DbContext.
-
Open
Program.cs- Show how Minimal APIs are configured.
- Explain DI wiring:
AddApplication(),AddInfrastructure(). - Show Swagger/OpenAPI configuration.
- Show the landing page and why it’s handy.
-
Walk through
UsersEndpoints.cs- Show each endpoint:
POST /api/usersGET /api/usersGET /api/users/allGET /api/users/id/{id}PUT /api/users/id/{id}DELETE /api/users/id/{id}POST /api/users/id/{id}/upload
- Map each to the corresponding HTTP method and REST operation.
- Show each endpoint:
-
Open
UserService.csandUserRepository.cs- Show the separation between:
- API (transport)
- Application (business logic)
- Infrastructure (EF Core queries)
- Show how filters & pagination are implemented.
- Show the separation between:
-
Demo live with Postman or Swagger
- Create a new user.
- List users (paged & unpaged).
- Get a user by ID.
- Update a user.
- Upload a profile picture and inspect:
- File on disk under
uploads/profilepic - URL stored in
profile_pic_urlcolumn.
- File on disk under
- Delete a user.
-
Tie back to REST
- Show how verbs + URLs + status codes map to REST constraints.
- Discuss statelessness and how JWT auth will fit in later.
After this base training project, natural extensions include:
-
Authentication & Authorization
- Add
POST /api/auth/login - Issue JWT tokens
- Protect endpoints using roles (
Admin,read_only)
- Add
-
Better Pagination
- Introduce a
PagedResult<T>wrapper withtotalCount,skip,take,hasMore.
- Introduce a
-
Validation & Error Handling
- Add FluentValidation or similar for DTOs.
- Implement centralized error handling middleware and consistent error responses.
-
More Resources
- Add additional entities (e.g.,
roles,permissions,audit_logs) following the same pattern. - Show how the structure scales to 50–100+ tables.
- Add additional entities (e.g.,
-
Observability & Ops
- Add structured logging, correlation IDs, health checks, etc.
-
Front-end Client
- Build a small SPA or simple web UI that consumes this API.
This README.md is meant to serve as both:
- A specification of the current implementation for the training demo
- A guide that trainees and reviewers can read to understand the architecture, design decisions, and how to run the project locally.