Introduction
This article shows how to build a production-minded Expense Tracker web application using ASP.NET Core (backend) and Angular (frontend). The app supports:
User authentication (JWT)
CRUD for expenses and categories
Monthly summaries and charts
Recurring expenses and simple budget alerts
Export to CSV and basic reports
I use simple Indian English, practical code examples, and Angular-focused implementation details. You can copy-paste code and adapt it for your organisation.
High-level architecture
[Angular SPA] <---HTTPS/JWT---> [ASP.NET Core Web API] <----> [SQL Server]
| |
+---(optional SignalR for live updates)---+
+---(blob storage for receipts & exports)-->[Blob Storage]
Frontend responsibilities: user UI, charts, client-side validation, and calling APIs. Backend responsibilities: authentication, business rules, data persistence, export generation, and scheduled jobs for recurring expenses.
Database design (simple and extensible)
Main tables:
Users(Id, UserName, Email, PasswordHash, CreatedAt)
Categories(Id, UserId, Name, Type) — Type = Expense or Income
Expenses(Id, UserId, CategoryId, Amount, Currency, Date, Description, IsRecurring, RecurrenceRule, ReceiptUrl, CreatedAt)
Budgets(Id, UserId, CategoryId, Month, Year, LimitAmount)
Notes
Use UserId for multi-user separation.
RecurrenceRule can store a simple cron-like string or RFC 5545 recurrence rule for future enhancements.
Index UserId, Date, CategoryId for fast reports.
ERD (text)
Users (1) --- (M) Categories
Users (1) --- (M) Expenses
Categories (1) --- (M) Expenses
Users (1) --- (M) Budgets
Backend: Project setup and packages
Create project:
dotnet new webapi -n ExpenseBackend
cd ExpenseBackend
Recommended packages:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
Create ExpenseDbContext with DbSet<User>, DbSet<Category>, DbSet<Expense>, DbSet<Budget>.
Backend: EF Core models (simplified)
User.cs
public class User
{
public Guid Id { get; set; }
public string UserName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string PasswordHash { get; set; } = string.Empty;
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
Category.cs
public class Category
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = "Expense"; // or "Income"
}
Expense.cs
public class Expense
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public Guid CategoryId { get; set; }
public decimal Amount { get; set; }
public string Currency { get; set; } = "INR";
public DateTime Date { get; set; }
public string? Description { get; set; }
public bool IsRecurring { get; set; }
public string? RecurrenceRule { get; set; }
public string? ReceiptUrl { get; set; }
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
Budget.cs
public class Budget
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public Guid? CategoryId { get; set; }
public int Month { get; set; }
public int Year { get; set; }
public decimal LimitAmount { get; set; }
}
Backend: DTOs and services
Keep controllers thin and business logic in services. Use DTOs for API contracts.
ExpenseCreateDto
public class ExpenseCreateDto { public Guid CategoryId; public decimal Amount; public string Currency; public DateTime Date; public string? Description; public bool IsRecurring; public string? RecurrenceRule; }
IExpenseService
public interface IExpenseService
{
Task<ExpenseDto> CreateExpenseAsync(Guid userId, ExpenseCreateDto dto);
Task<PagedResult<ExpenseDto>> GetExpensesAsync(Guid userId, ExpenseQuery query);
Task<SummaryDto> GetMonthlySummary(Guid userId, int month, int year);
Task ExportCsvAsync(Guid userId, ExpenseQuery query, Stream output);
}
Implementation notes:
For GetMonthlySummary, use server-side aggregation grouped by category.
For ExportCsvAsync stream CSV to avoid memory pressure.
Backend: Authentication (JWT)
Use JWT for API and Angular to store token in memory or secure storage. Example Program.cs snippet:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters { /* issuer, key, audience */ };
});
app.UseAuthentication();
app.UseAuthorization();
Expose /api/auth/login and /api/auth/register endpoints. On login return { token: "..." }.
Security tips:
Backend: Monthly summary query example (EF Core)
public async Task<SummaryDto> GetMonthlySummary(Guid userId, int month, int year)
{
var start = new DateTime(year, month, 1);
var end = start.AddMonths(1);
var items = await _db.Expenses
.Where(e => e.UserId == userId && e.Date >= start && e.Date < end)
.GroupBy(e => e.CategoryId)
.Select(g => new { CategoryId = g.Key, Total = g.Sum(x => x.Amount) })
.ToListAsync();
// map category names
return new SummaryDto { Totals = items.Select(i => new CategoryTotal { CategoryId = i.CategoryId, Total = i.Total }).ToList() };
}
Frontend: Angular setup and libraries
Create Angular project and install helpful libraries:
ng new expense-tracker
cd expense-tracker
npm install chart.js ngx-chartjs @auth0/angular-jwt file-saver
Use ngx-chartjs or ng2-charts to render charts. Use @auth0/angular-jwt for token helpers.
Project structure suggestion
src/app/core (auth, http interceptor)
src/app/shared (models, components)
src/app/expenses (list, edit, service)
src/app/dashboard (summary, charts)
Frontend: AuthService and interceptor
auth.service.ts provides login/register and token storage.
auth.interceptor.ts attaches token to each request.
Important: store token in memory or localStorage depending on threat model. For production consider short-lived tokens with refresh flow.
Frontend: ExpenseService (detailed)
expense.service.ts handles API calls and exposes Observables for updates.
@Injectable({providedIn: 'root'})
export class ExpenseService {
private base = '/api/expenses';
constructor(private http: HttpClient) {}
getExpenses(query: any) { return this.http.get<PagedResult<Expense>>(`${this.base}`, { params: query }); }
createExpense(dto: any) { return this.http.post<Expense>(this.base, dto); }
updateExpense(id: string, dto: any) { return this.http.put(`${this.base}/${id}`, dto); }
deleteExpense(id: string) { return this.http.delete(`${this.base}/${id}`); }
getMonthlySummary(month: number, year: number) { return this.http.get<Summary>(`${this.base}/summary`, { params: { month, year } }); }
exportCsv(query: any) { return this.http.get(`${this.base}/export`, { params: query, responseType: 'blob' }); }
}
Frontend: Expense list and editor component
list component shows pagination, filters, and actions. Use server-side pagination.
editor component can be a reactive form with fields: amount, category, date, description, receipt (file input), isRecurring.
File upload: upload receipts to /api/expenses/{id}/receipt using FormData and show preview after upload.
Frontend: Dashboard with charts
Create dashboard.component.ts that calls getMonthlySummary and renders:
Example using Chart.js via wrapper
this.expenseService.getMonthlySummary(month, year).subscribe(data => {
this.pieData = data.totals.map(t => t.total);
this.pieLabels = data.totals.map(t => t.categoryName);
});
Chart best practices:
Limit points on line chart (use daily totals, not every transaction)
Use tooltips for amounts
Avoid heavy client-side computation on large datasets; aggregate on server.
Recurring expenses and scheduler
Store recurrence metadata in Expense.RecurrenceRule. Use a background job (Hangfire, Quartz.NET, or Azure Functions Timer) to create instances for next period.
Example schedule using Hangfire:
Budget alerts and notifications
For each budget, calculate total spent and compare with LimitAmount. Send notifications when > 80% and 100%.
Notification options:
Export CSV and reports
Implement streaming CSV on server to avoid OOM. Example using CsvHelper:
public async Task ExportCsvAsync(Guid userId, ExpenseQuery q, Stream output)
{
var expenses = GetQuery(userId, q).AsNoTracking().AsAsyncEnumerable();
using var writer = new StreamWriter(output);
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
await csv.WriteRecordsAsync(expenses);
}
On client call expenseService.exportCsv(...) and use FileSaver.saveAs to prompt download.
Security and production best practices
Always use HTTPS.
Validate inputs server-side and enforce authorization per UserId.
Use parameterized queries / EF Core to avoid SQL injection.
Rate limit export and heavy endpoints.
Sanitize any HTML or user-supplied content.
Properly configure CORS for your SPA origin.
Use centralized logging, structured logs and monitor errors.
Performance tips
Aggregate on the server for charts and reports.
Use caching for reference data (categories) using Redis or in-memory cache.
Use database indices on Date and UserId.
Paginate and limit results.
Testing and monitoring
Unit test services with in-memory DB (EF Core InMemory) or SQLite.
Integration tests using WebApplicationFactory.
E2E tests for Angular using Cypress.
Monitor API latency and error rates with Application Insights or Prometheus/Grafana.
Deployment checklist
Configure connection strings and secret keys via environment variables or key vault.
Configure auto migrations or run migrations at deploy time.
Provision storage for receipts and exports.
Configure CI/CD: build backend (dotnet publish), build Angular (ng build --prod), and deploy static files to CDN or serve via backend.
Sample sequence diagram (create expense)
User -> Angular UI -> POST /api/expenses -> API validates -> Save DB -> Return 201 -> UI refresh list -> Dashboard update
Final notes and next steps
This article shows a full, production-focused plan to build an Expense Tracker with ASP.NET Core and Angular. Next improvements:
Multi-currency support with exchange rates
Advanced budgeting rules and alerts
Machine learning for expense categorization
Mobile apps with same API
If you want, I can now:
generate a ready-to-run GitHub scaffold for backend and frontend,
add EF Core migrations, seed data and sample SQL,
produce Angular components and unit tests,
add PNG/SVG architecture and sequence diagrams.
Tell me which one you want next.