A multi-user expense tracking backend with workspace collaboration, category management, and historical exchange rates.
Stack: NestJS · TypeScript · Apollo GraphQL · Prisma · PostgreSQL · Redis
Prerequisites: Docker, Node.js
# 1. Start Postgres (port 9992) and Redis (port 6380)
cd docker && docker compose up -d
# 2. Install dependencies and set up the database
npm i
npm run prisma:client:generate
npm run prisma:migration:up
# 3. Start the dev server (port 9998)
npm run start:devGraphQL playground: http://localhost:9998/graphql
The API is primarily GraphQL (code-first, Apollo). A small number of session and verification flows are exposed as REST endpoints.
| Protocol | Base URL |
|---|---|
| GraphQL | http://localhost:9998/graphql |
| REST | http://localhost:9998 |
Most GraphQL operations require a valid session. Authentication is cookie-based:
- Call
POST /auth/loginwith email and password - Receive an
accessTokenin the response body and arefreshTokenin anhttpOnlycookie - Pass the access token on subsequent GraphQL requests:
Authorization: Bearer <accessToken> - When the access token expires, call
POST /auth/refresh-tokento get a new pair
See the Auth module for the full endpoint reference.
| Role | Access |
|---|---|
USER |
Can access and modify their own data only |
ADMIN |
Full access to all resources |
Operations that are ADMIN-only are noted in each module's documentation.
Three currencies are supported throughout the API:
| Code | Description |
|---|---|
BYN |
Belarusian Ruble (base currency for rate calculations) |
USD |
US Dollar |
EUR |
Euro |
Custom GraphQL scalars used across the schema:
| Scalar | Format | Example |
|---|---|---|
Date |
YYYY-MM-DD |
"2024-03-15" |
DateIso |
ISO 8601 | "2024-03-15T10:30:00.000Z" |
Decimal |
Numeric string | "49.99" |
JSON |
Any JSON value | { "key": "value" } |
| Module | Description | Docs |
|---|---|---|
auth |
Login, token refresh, logout | → |
user |
Registration, profile management, admin controls | → |
confirm-email |
Email address verification flow | → |
resend-email |
Re-send verification or password reset emails | → |
reset-password |
Forgot password / reset password flow | → |
| Module | Description | Docs |
|---|---|---|
contact |
Contacts, invites (by ID or email), and user blocking | → |
| Module | Description | Docs |
|---|---|---|
workspace |
Top-level expense containers | → |
item |
Expense categories within a workspace | → |
payment |
Individual expense records | → |
tag |
Labels for items | → |
item-tag |
Assign / unassign tags on items | → |
item-extract |
Split payments from one item into a new item | → |
item-merge |
Consolidate two items into one | → |
| Module | Description | Docs |
|---|---|---|
items-aggregation |
Item counts and rollup metrics across a workspace or tag | → |
payments-aggregation |
Payment totals by currency, date range, and default currency | → |
currency-rate |
Historical exchange rates (NBRB, cached) | → |
| Module | Description | Docs |
|---|---|---|
workspace-history |
Read-only audit log of all workspace mutations | → |
All application errors follow a consistent GraphQL error shape:
{
"errors": [
{
"message": "Human-readable description",
"extensions": {
"code": "APPLICATION_ERROR_CODE",
"status": 400
}
}
]
}Each module's documentation lists the error codes it can produce.
The API does not use cursor or offset pagination — queries return full result sets. Filtering is available on most list operations via ItemsFilter and PaymentsFilter arguments:
items(workspaceId: 3, itemsFilter: { tagIds: [1], title: "food" }, paymentsFilter: { dateFrom: "2024-01-01" }) { ... }Aggregated metrics are available at multiple levels of the data hierarchy without extra queries:
Workspace
└── itemsAggregation
├── count
└── paymentsAggregation
├── count
├── costByCurrency { USD EUR BYN }
├── costInDefaultCurrency
├── firstPaymentDate
└── lastPaymentDate
The same structure is available on Tag and on individual Item nodes. All aggregations accept PaymentsFilter to scope the date range.
Every mutation on workspace data (items, payments, tags, etc.) automatically creates a history entry. History is accessible via Workspace.history and includes a human-readable message, a structured changes diff, and the actor who performed the action.
See workspace-history for the full field reference.