Build a complete bank/government data system from scratch โ database โ backend โ API โ frontend.
We will build a Financial Records API โ the same pattern used by banks and government agencies to expose data securely. The system will have:
/api/accountshttps://localhost:7000.dotnet --version # should print 8.x.x dotnet new webapi --help # confirms templates available
FinanceApi/ โโโ Controllers/ โ โโโ AccountsController.cs # HTTP endpoints โ โโโ TransactionsController.cs โโโ Models/ โ โโโ Account.cs # C# data classes โ โโโ Transaction.cs โโโ Data/ โ โโโ AppDbContext.cs # EF Core database context โโโ Services/ โ โโโ AccountService.cs # business logic โโโ DTOs/ โ โโโ AccountDto.cs # data transfer objects โโโ Program.cs # app startup + DI โโโ appsettings.json # connection string โโโ frontend/ โโโ index.html # your browser UI
# Create Web API project dotnet new webapi -n FinanceApi --use-controllers cd FinanceApi # Install Entity Framework Core + SQL Server provider dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools dotnet add package Microsoft.EntityFrameworkCore.Design # Install Swagger for API testing dotnet add package Swashbuckle.AspNetCore
This tells EF Core where your SQL Server LocalDB lives. LocalDB creates a .mdf file in your user folder automatically.
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=FinanceDb;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"AllowedHosts": "*"
}
"Server=.\\SQLEXPRESS;Database=FinanceDb;Trusted_Connection=True"Each C# class becomes a SQL table. Each property becomes a column. EF Core handles this mapping automatically.
using System.ComponentModel.DataAnnotations; namespace FinanceApi.Models; public class Account { public int Id { get; set; } // PRIMARY KEY (auto-increment) [Required, MaxLength(100)] public string OwnerName { get; set; } = ""; [Required, MaxLength(20)] public string AccountNumber { get; set; } = ""; public decimal Balance { get; set; } public string AccountType { get; set; } = "Checking"; // Savings, Govt public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public bool IsActive { get; set; } = true; // Navigation property โ one account has many transactions public ICollection<Transaction> Transactions { get; set; } = []; }
namespace FinanceApi.Models; public class Transaction { public int Id { get; set; } public int AccountId { get; set; } // FOREIGN KEY public Account Account { get; set; } = null!; public decimal Amount { get; set; } public string Type { get; set; } = "Credit"; // Debit, Transfer public string Description { get; set; } = ""; public DateTime Date { get; set; } = DateTime.UtcNow; public string Status { get; set; } = "Completed"; // Pending, Failed }
The DbContext is the heart of EF Core. It maps your C# classes to SQL tables and handles all queries.
using Microsoft.EntityFrameworkCore; using FinanceApi.Models; namespace FinanceApi.Data; public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } // These become SQL tables automatically public DbSet<Account> Accounts => Set<Account>(); public DbSet<Transaction> Transactions => Set<Transaction>(); protected override void OnModelCreating(ModelBuilder mb) { // Precision for money โ important for banking! mb.Entity<Account>() .Property(a => a.Balance) .HasPrecision(18, 2); mb.Entity<Transaction>() .Property(t => t.Amount) .HasPrecision(18, 2); // Seed data โ so DB is not empty on first run mb.Entity<Account>().HasData( new Account { Id=1, OwnerName="Ministry of Finance", AccountNumber="GOV-001", Balance=9_500_000, AccountType="Government" }, new Account { Id=2, OwnerName="Central Bank Reserve", AccountNumber="CB-001", Balance=250_000_000, AccountType="Reserve" }, new Account { Id=3, OwnerName="John Citizen", AccountNumber="ACC-8821", Balance=4_200.50m, AccountType="Checking" } ); } }
EF Core will read your C# models and generate SQL CREATE TABLE statements automatically. You never write raw DDL.
# Generate migration files (C# code that describes DB changes) dotnet ef migrations add InitialCreate # Apply the migration โ this creates the SQL tables in LocalDB dotnet ef database update # You should see: "Done." in terminal # Tables created: Accounts, Transactions, __EFMigrationsHistory
(localdb)\MSSQLLocalDB โ you will see your FinanceDb database with the tables already created and seed data inserted.DTOs (Data Transfer Objects) are what you expose via the API. Never expose your DB model directly โ DTOs let you control exactly what data the browser sees.
namespace FinanceApi.DTOs; // What the API returns to the browser public record AccountDto( int Id, string OwnerName, string AccountNumber, decimal Balance, string AccountType, bool IsActive, DateTime CreatedAt ); // What the browser sends to CREATE an account public record CreateAccountDto( string OwnerName, string AccountNumber, decimal InitialBalance, string AccountType ); // Transaction DTO public record TransactionDto( int Id, int AccountId, decimal Amount, string Type, string Description, DateTime Date, string Status );
The service is a plain C# class. It uses Entity Framework Core to query SQL Server using LINQ โ no raw SQL needed.
using Microsoft.EntityFrameworkCore; using FinanceApi.Data; using FinanceApi.DTOs; using FinanceApi.Models; namespace FinanceApi.Services; public interface IAccountService { Task<List<AccountDto>> GetAllAsync(string? type = null); Task<AccountDto?> GetByIdAsync(int id); Task<AccountDto> CreateAsync(CreateAccountDto dto); Task<bool> DeleteAsync(int id); Task<List<TransactionDto>> GetTransactionsAsync(int accountId); } public class AccountService : IAccountService { private readonly AppDbContext _db; public AccountService(AppDbContext db) => _db = db; // GET all accounts, optionally filtered by type public async Task<List<AccountDto>> GetAllAsync(string? type = null) { var query = _db.Accounts.AsQueryable(); if (!string.IsNullOrEmpty(type)) query = query.Where(a => a.AccountType == type); return await query .Select(a => new AccountDto( a.Id, a.OwnerName, a.AccountNumber, a.Balance, a.AccountType, a.IsActive, a.CreatedAt)) .ToListAsync(); } // GET single account by ID public async Task<AccountDto?> GetByIdAsync(int id) { var a = await _db.Accounts.FindAsync(id); if (a is null) return null; return new AccountDto(a.Id, a.OwnerName, a.AccountNumber, a.Balance, a.AccountType, a.IsActive, a.CreatedAt); } // POST โ create new account public async Task<AccountDto> CreateAsync(CreateAccountDto dto) { var account = new Account { OwnerName = dto.OwnerName, AccountNumber = dto.AccountNumber, Balance = dto.InitialBalance, AccountType = dto.AccountType }; _db.Accounts.Add(account); await _db.SaveChangesAsync(); // writes to SQL Server return new AccountDto(account.Id, account.OwnerName, account.AccountNumber, account.Balance, account.AccountType, account.IsActive, account.CreatedAt); } // DELETE account public async Task<bool> DeleteAsync(int id) { var account = await _db.Accounts.FindAsync(id); if (account is null) return false; _db.Accounts.Remove(account); await _db.SaveChangesAsync(); return true; } // GET transactions for an account public async Task<List<TransactionDto>> GetTransactionsAsync(int accountId) { return await _db.Transactions .Where(t => t.AccountId == accountId) .OrderByDescending(t => t.Date) .Select(t => new TransactionDto( t.Id, t.AccountId, t.Amount, t.Type, t.Description, t.Date, t.Status)) .ToListAsync(); } }
This is the entry point. Register your services, set up CORS (so your HTML page can call the API), add Swagger for testing.
using Microsoft.EntityFrameworkCore; using FinanceApi.Data; using FinanceApi.Services; var builder = WebApplication.CreateBuilder(args); // โโ Database โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ builder.Services.AddDbContext<AppDbContext>(opt => opt.UseSqlServer(builder.Configuration .GetConnectionString("DefaultConnection"))); // โโ Dependency Injection โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ builder.Services.AddScoped<IAccountService, AccountService>(); // โโ CORS โ allow HTML file to call this API โโโโโโโโโโโโโโโ builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => policy.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod()); }); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new { Title = "Finance API", Version = "v1" }); }); var app = builder.Build(); // โโ Auto-apply migrations on startup โโโโโโโโโโโโโโโโโโโโโ using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); db.Database.Migrate(); // creates DB if it doesn't exist } // โโ Middleware pipeline โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); // visit /swagger to test API } app.UseCors("AllowAll"); // MUST be before MapControllers app.UseHttpsRedirection(); app.MapControllers(); app.Run();
Controllers define your REST routes. Each method maps to a HTTP verb (GET, POST, PUT, DELETE). The controller calls the service โ it does NO business logic itself.
using Microsoft.AspNetCore.Mvc; using FinanceApi.DTOs; using FinanceApi.Services; namespace FinanceApi.Controllers; [ApiController] [Route("api/[controller]")] // โ /api/accounts public class AccountsController : ControllerBase { private readonly IAccountService _svc; public AccountsController(IAccountService svc) => _svc = svc; // GET /api/accounts โ all accounts // GET /api/accounts?type=Government โ filtered [HttpGet] public async Task<IActionResult> GetAll([FromQuery] string? type) { var accounts = await _svc.GetAllAsync(type); return Ok(accounts); // 200 + JSON array } // GET /api/accounts/3 โ single account [HttpGet("{id}")] public async Task<IActionResult> GetById(int id) { var account = await _svc.GetByIdAsync(id); return account is null ? NotFound() : Ok(account); } // POST /api/accounts โ create account [HttpPost] public async Task<IActionResult> Create([FromBody] CreateAccountDto dto) { if (!ModelState.IsValid) return BadRequest(ModelState); var created = await _svc.CreateAsync(dto); return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); // 201 Created } // DELETE /api/accounts/3 [HttpDelete("{id}")] public async Task<IActionResult> Delete(int id) { var deleted = await _svc.DeleteAsync(id); return deleted ? NoContent() : NotFound(); // 204 or 404 } // GET /api/accounts/3/transactions [HttpGet("{id}/transactions")] public async Task<IActionResult> GetTransactions(int id) { var txns = await _svc.GetTransactionsAsync(id); return Ok(txns); } }
dotnet run # Output: # Now listening on: https://localhost:7000 # Now listening on: http://localhost:5000 # Test it immediately: curl https://localhost:7000/api/accounts # Returns JSON array of accounts from your SQL Server DB!
Save this as frontend/index.html and open it in your browser. It connects directly to your local ASP.NET API.
<!-- Full bank/government data portal frontend -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Finance Portal</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', sans-serif; background: #f0f4f8; color: #1e293b; }
header {
background: linear-gradient(135deg, #1e3a5f, #0f2340);
color: white; padding: 20px 32px;
display: flex; align-items: center; gap: 16px;
}
header h1 { font-size: 22px; font-weight: 700; }
header p { font-size: 13px; opacity: 0.7; margin-top: 2px; }
.status-dot { width: 10px; height: 10px; border-radius: 50%;
background: #10b981; margin-left: auto; animation: pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
.toolbar {
background: white; border-bottom: 1px solid #e2e8f0;
padding: 12px 32px; display: flex; gap: 8px; flex-wrap: wrap;
}
button {
padding: 8px 16px; border: 1px solid #cbd5e1; border-radius: 6px;
cursor: pointer; font-size: 13px; font-weight: 500; background: white;
transition: all 0.15s;
}
button:hover { background: #1e3a5f; color: white; border-color: #1e3a5f; }
button.primary { background: #1e3a5f; color: white; border-color: #1e3a5f; }
button.danger { background: #dc2626; color: white; border-color: #dc2626; }
input, select {
padding: 8px 12px; border: 1px solid #cbd5e1; border-radius: 6px;
font-size: 13px; min-width: 160px;
}
main { padding: 24px 32px; }
#status { margin-bottom: 16px; font-size: 13px; color: #64748b; }
#status.error { color: #dc2626; }
#status.success { color: #059669; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.card {
background: white; border-radius: 10px; padding: 20px;
border: 1px solid #e2e8f0; transition: box-shadow 0.2s;
}
.card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.08); }
.card-header { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.avatar {
width: 42px; height: 42px; border-radius: 8px; display: flex;
align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0;
}
.av-gov { background: #dbeafe; }
.av-bank { background: #fef3c7; }
.av-user { background: #d1fae5; }
.card h3 { font-size: 15px; font-weight: 600; }
.card .acn { font-size: 12px; color: #94a3b8; font-family: monospace; }
.balance { font-size: 22px; font-weight: 700; color: #1e293b; margin: 10px 0 4px; }
.balance.large { color: #1d4ed8; }
.badge {
display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px;
font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
}
.badge.govt { background: #dbeafe; color: #1d4ed8; }
.badge.bank { background: #fef3c7; color: #92400e; }
.badge.check { background: #d1fae5; color: #065f46; }
.badge.save { background: #f3e8ff; color: #6b21a8; }
.card-footer { display: flex; gap: 8px; margin-top: 14px; }
.modal-overlay {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,0.5); z-index: 100;
align-items: center; justify-content: center;
}
.modal-overlay.open { display: flex; }
.modal {
background: white; border-radius: 12px; padding: 28px;
width: 420px; max-width: 92vw;
}
.modal h2 { font-size: 18px; font-weight: 700; margin-bottom: 20px; }
.field { margin-bottom: 14px; }
.field label { display: block; font-size: 12px; font-weight: 600;
color: #64748b; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
.field input, .field select { width: 100%; }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; }
#txn-panel {
display: none; background: white; border: 1px solid #e2e8f0;
border-radius: 10px; margin-bottom: 20px; overflow: hidden;
}
#txn-panel.open { display: block; }
#txn-header { background: #1e3a5f; color: white; padding: 14px 20px; font-weight: 600; font-size: 14px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { text-align: left; padding: 10px 16px; background: #f8fafc;
color: #64748b; font-weight: 600; border-bottom: 1px solid #e2e8f0; font-size: 11px; text-transform: uppercase; }
td { padding: 10px 16px; border-bottom: 1px solid #f1f5f9; }
tr:last-child td { border-bottom: none; }
.credit { color: #059669; font-weight: 600; }
.debit { color: #dc2626; font-weight: 600; }
.loading { text-align: center; padding: 40px; color: #94a3b8; font-size: 14px; }
.empty { text-align: center; padding: 60px; color: #94a3b8; }
.empty .icon { font-size: 48px; margin-bottom: 12px; }
</style>
</head>
<body>
<header>
<div>
<h1>๐ฆ Finance Data Portal</h1>
<p>Connected to ASP.NET Core API โ localhost:7000</p>
</div>
<div class="status-dot" id="statusDot" title="API Status"></div>
</header>
<div class="toolbar">
<button class="primary" onclick="loadAccounts()">๐ Refresh</button>
<button onclick="openModal()">โ New Account</button>
<select id="typeFilter" onchange="loadAccounts()">
<option value="">All Types</option>
<option value="Government">Government</option>
<option value="Reserve">Reserve</option>
<option value="Checking">Checking</option>
<option value="Savings">Savings</option>
</select>
<input id="search" placeholder="Search by name..." oninput="filterCards()" />
</div>
<main>
<div id="status">Loading accounts...</div>
<div id="txn-panel">
<div id="txn-header">Transactions</div>
<table>
<thead><tr>
<th>Date</th><th>Description</th>
<th>Type</th><th>Amount</th><th>Status</th>
</tr></thead>
<tbody id="txn-body"></tbody>
</table>
</div>
<div class="grid" id="accountGrid">
<div class="loading">โณ Connecting to API...</div>
</div>
</main>
<!-- CREATE ACCOUNT MODAL -->
<div class="modal-overlay" id="modal">
<div class="modal">
<h2>โ Create New Account</h2>
<div class="field">
<label>Owner Name</label>
<input id="f-name" placeholder="e.g. Ministry of Transport" />
</div>
<div class="field">
<label>Account Number</label>
<input id="f-acn" placeholder="e.g. GOV-002" />
</div>
<div class="field">
<label>Initial Balance ($)</label>
<input id="f-bal" type="number" placeholder="0.00" />
</div>
<div class="field">
<label>Account Type</label>
<select id="f-type">
<option>Government</option>
<option>Reserve</option>
<option>Checking</option>
<option>Savings</option>
</select>
</div>
<div class="modal-actions">
<button onclick="closeModal()">Cancel</button>
<button class="primary" onclick="createAccount()">Create Account</button>
</div>
</div>
</div>
<script>
const API = 'https://localhost:7000/api'; // โ change port if needed
let allCards = [];
// โโ LOAD ACCOUNTS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function loadAccounts() {
const type = document.getElementById('typeFilter').value;
const url = type ? `${API}/accounts?type=${type}` : `${API}/accounts`;
setStatus('Loading...', '');
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`API error: ${res.status}`);
const data = await res.json();
renderCards(data);
setStatus(`โ
${data.length} accounts loaded`, 'success');
document.getElementById('statusDot').style.background = '#10b981';
} catch (err) {
setStatus(`โ Cannot reach API: ${err.message}. Is the API running?`, 'error');
document.getElementById('statusDot').style.background = '#ef4444';
document.getElementById('accountGrid').innerHTML =
`<div class="empty"><div class="icon">โ ๏ธ</div>
Could not connect to API.<br>Run: <code>dotnet run</code></div>`;
}
}
// โโ RENDER CARDS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function renderCards(accounts) {
allCards = accounts;
if (!accounts.length) {
document.getElementById('accountGrid').innerHTML =
`<div class="empty"><div class="icon">๐ฆ</div>No accounts found.</div>`;
return;
}
document.getElementById('accountGrid').innerHTML = accounts.map(a => {
const icons = { Government:'๐๏ธ', Reserve:'๐ฆ', Checking:'๐ณ', Savings:'๐ช' };
const avClass = { Government:'av-gov', Reserve:'av-bank',
Checking:'av-user', Savings:'av-user' };
const badgeClass = { Government:'govt', Reserve:'bank',
Checking:'check', Savings:'save' };
const big = a.balance > 1_000_000;
return `
<div class="card" id="card-${a.id}">
<div class="card-header">
<div class="avatar ${avClass[a.accountType]||'av-user'}">
${icons[a.accountType]||'๐ค'}
</div>
<div>
<div class="card-h3"><strong>${a.ownerName}</strong></div>
<div class="acn">${a.accountNumber}</div>
</div>
</div>
<div class="balance ${big?'large':''}">
$${a.balance.toLocaleString('en-US', {minimumFractionDigits:2})}
</div>
<span class="badge ${badgeClass[a.accountType]||'check'}">
${a.accountType}
</span>
<span style="font-size:11px;color:#94a3b8;margin-left:8px;">
${a.isActive ? 'โ
Active' : 'โ Inactive'}
</span>
<div class="card-footer">
<button onclick="viewTransactions(${a.id}, '${a.ownerName}')">
๐ Transactions
</button>
<button class="danger" onclick="deleteAccount(${a.id})">๐</button>
</div>
</div>`;
}).join('');
}
// โโ SEARCH FILTER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function filterCards() {
const q = document.getElementById('search').value.toLowerCase();
const filtered = allCards.filter(a =>
a.ownerName.toLowerCase().includes(q) ||
a.accountNumber.toLowerCase().includes(q));
renderCards(filtered);
}
// โโ VIEW TRANSACTIONS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function viewTransactions(id, name) {
const panel = document.getElementById('txn-panel');
document.getElementById('txn-header').textContent = `Transactions โ ${name}`;
document.getElementById('txn-body').innerHTML = '<tr><td colspan="5" class="loading">Loading...</td></tr>';
panel.classList.add('open');
panel.scrollIntoView({ behavior: 'smooth' });
try {
const res = await fetch(`${API}/accounts/${id}/transactions`);
const txns = await res.json();
if (!txns.length) {
document.getElementById('txn-body').innerHTML =
'<tr><td colspan="5" class="loading">No transactions yet.</td></tr>';
return;
}
document.getElementById('txn-body').innerHTML = txns.map(t => `
<tr>
<td>${new Date(t.date).toLocaleDateString()}</td>
<td>${t.description || 'โ'}</td>
<td>${t.type}</td>
<td class="${t.type==='Credit'?'credit':'debit'}">
${t.type==='Credit'?'+':'-'}$${t.amount.toFixed(2)}
</td>
<td>${t.status}</td>
</tr>`).join('');
} catch { document.getElementById('txn-body').innerHTML = '<tr><td colspan="5">Error loading.</td></tr>'; }
}
// โโ CREATE ACCOUNT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function createAccount() {
const body = {
ownerName: document.getElementById('f-name').value,
accountNumber: document.getElementById('f-acn').value,
initialBalance: parseFloat(document.getElementById('f-bal').value) || 0,
accountType: document.getElementById('f-type').value
};
if (!body.ownerName || !body.accountNumber) {
alert('Name and account number are required.'); return;
}
try {
const res = await fetch(`${API}/accounts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) throw new Error(await res.text());
closeModal();
await loadAccounts();
setStatus('โ
Account created successfully!', 'success');
} catch (err) { alert('Error: ' + err.message); }
}
// โโ DELETE ACCOUNT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function deleteAccount(id) {
if (!confirm('Delete this account?')) return;
try {
const res = await fetch(`${API}/accounts/${id}`, { method: 'DELETE' });
if (res.ok) { await loadAccounts(); setStatus('๐๏ธ Account deleted.', 'success'); }
else setStatus('Could not delete.', 'error');
} catch { setStatus('API error.', 'error'); }
}
// โโ HELPERS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg; el.className = cls;
}
function openModal() { document.getElementById('modal').classList.add('open'); }
function closeModal() { document.getElementById('modal').classList.remove('open'); }
document.getElementById('modal').onclick = e =>
{ if (e.target === e.currentTarget) closeModal(); };
loadAccounts();
</script>
</body>
</html>
# 1. Create the project dotnet new webapi -n FinanceApi --use-controllers cd FinanceApi # 2. Install packages dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Swashbuckle.AspNetCore # 3. Add all the files (Models, Data, Services, Controllers, DTOs) # โ copy all code from tabs 2, 3, 4 # 4. Run migrations (creates SQL Server DB + tables) dotnet ef migrations add InitialCreate dotnet ef database update # 5. Run the API dotnet run # 6. Open Swagger to test API # โ https://localhost:7000/swagger # 7. Open frontend/index.html in browser # โ Double-click the file or use Live Server extension in VS Code
Here is what happens when you click "Refresh" in the browser:
fetch("https://localhost:7000/api/accounts")/api/accounts โ AccountsController.GetAll()_svc.GetAllAsync() โ the service layer_db.Accounts.Select(...).ToListAsync()SELECT * FROM AccountsOk(accounts) โ HTTP 200 with JSON bodyrenderCards(data), cards appearapp.UseCors("AllowAll") is in Program.cs BEFORE app.MapControllers(). Order matters.dotnet dev-certs https --trust in terminal, then restart browser.SqlLocalDB start in terminal, or open Visual Studio once (it starts LocalDB automatically).http://localhost:5000/api and use the HTTP port shown in dotnet run output.Microsoft.AspNetCore.Authentication.JwtBearer to require JWT tokens (how banks actually secure APIs)?page=1&size=20 query params to avoid loading 10,000 rowsFluentValidation package for complex business rulesILogger<T> injected into services (already built into ASP.NET)xUnit + mock AppDbContext with in-memory providerdotnet publish + Azure SQL for the DB