i-Bank
A banking API built around a strict auth boundary
NestJS backend for a banking client — JWT authentication via Passport, guarded account operations, and separated profile and preferences domains.
4
Feature modules
JWT + Passport
Auth
Auth service & controller
Tested
Overview
i-Bank is a NestJS API for banking operations, built as a study in getting the authentication boundary right before building anything on top of it.
Where DealPlate delegates identity wholesale to Firebase, i-Bank issues and verifies its own JWTs through Passport — a deliberately different approach to the same problem, and the reason the project was worth building.
Problem
Financial APIs have no forgiving failure mode. An endpoint that's accidentally public in a social app is embarrassing; the same mistake over account data is a breach.
The usual failure isn't a missing auth check — it's an inconsistent one: guards applied per controller, a helper someone forgets to call, a route added later that nobody remembers to protect.
Solution
Authentication is structural. A Passport JWT strategy validates tokens, a JwtAuthGuard applies the check, and protected routes declare that guard rather than each handler re-deriving the caller's identity.
Domains are kept narrow and separate — accounts, profile and preferences are distinct modules, so account operations don't quietly grow profile-editing powers.
DTOs with class-validator define the request contract explicitly, and the auth service and controller are the parts that carry unit tests, because they're the parts where a regression is most expensive.
Features
JWT authentication
Token issuance and verification through @nestjs/jwt with a Passport strategy, rather than hand-rolled token handling.
Guarded routes
JwtAuthGuard applied as a structural concern, so protection is declared rather than remembered.
Account operations
A dedicated accounts module holding balance and account-level logic behind the auth boundary.
Profile management
User profile handling kept separate from account operations.
Preferences
Its own module — settings shouldn't share a surface with money.
Validated DTOs
Typed request objects define the contract at the edge of every endpoint.
Firebase Admin integration
Available alongside the JWT strategy for identity verification where a federated token is the better fit.
Unit-tested auth
Spec coverage on the auth service and controller — the highest-consequence code in the project.
Tech Stack
Framework
- NestJS
- TypeScript
Auth
- @nestjs/jwt
- Passport
- passport-jwt
- Firebase Admin
Validation
- @nestjs/mapped-types
- DTOs
Runtime
- Node.js
- Express platform
Tooling
- ESLint flat config
- Jest
Architecture
A conventional layered NestJS service with the auth boundary pulled out as its own concern: strategy, guard and module, consumed by feature modules rather than reimplemented inside them.
auth/
JWT strategy, guard, controller, service and DTOs — plus the project's unit tests.
Passport@nestjs/jwtguards/
Shared guards available across modules, keeping protection composable.
NestJSaccounts/
Account operations behind the auth boundary, in controller/service/module form.
NestJSprofile/ + preferences/
Separate narrow domains, deliberately not merged into a single 'user' module.
NestJSconfig/
Environment and application configuration isolated from feature code.
@nestjs/config
Folder Structure
- src/
- NestJS application root
- ├── main.ts
- Bootstrap
- ├── app.module.ts
- Module composition
- ├── auth/
- jwt.strategy.ts, jwt-auth.guard.ts, controller, service, dto/, specs
- ├── guards/
- Shared guards used across feature modules
- ├── accounts/
- Account operations — controller, service, module
- ├── profile/
- Profile domain
- ├── preferences/
- User preferences domain
- └── config/
- Environment configuration
API Flow
Credentials in, token out, token verified on every subsequent call — with the verification applied structurally rather than per handler.
Client authenticates
The auth controller accepts credentials against a validated DTO.
Service issues a JWT
@nestjs/jwt signs a token carrying the user claim.
Client sends the bearer token
Subsequent requests carry it in the Authorization header.
Passport strategy validates
passport-jwt extracts and verifies the token, resolving the user from its payload.
JwtAuthGuard admits or rejects
Protected routes declare the guard; unauthenticated requests never reach a handler.
Feature service executes
Account, profile or preference logic runs with an already-established identity.
Challenges
Making protection impossible to forget
The realistic failure mode isn't writing a bad auth check, it's adding a route six weeks later and not writing one at all. Pushing verification into a guard and strategy means a new endpoint's protection is a declaration in its decorator rather than a block of code someone has to remember to paste.
Two identity systems in one codebase
The project carries both a self-issued JWT strategy and Firebase Admin. Keeping both meaningful meant being clear about which owns which surface, rather than letting them overlap into an ambiguous middle where it's unclear what actually authenticated a request.
Deciding what deserved tests
Testing everything in a project this size is a poor use of effort; testing nothing is worse. Coverage went to the auth service and controller, on the reasoning that a silent regression there is the only one that's genuinely dangerous.
Learnings
Passport strategies are worth the indirection — they turn 'is this request authenticated' into configuration rather than logic.
Narrow modules age better than broad ones. Splitting profile from preferences from accounts looked like overkill and stopped being overkill almost immediately.
Building a self-issued JWT flow after building a Firebase-delegated one clarified what each actually gives you: control versus not owning credential storage.
DTOs are documentation that can't drift, because the contract is enforced at runtime.
Future Improvements
Refresh token rotation with revocation — the obvious gap in any access-token-only design.
Persistence behind the account module, with transactional guarantees appropriate to balance changes.
An immutable transaction ledger rather than mutable balance fields.
Rate limiting and lockout on the authentication endpoints.
Integration tests covering the guard, not just unit tests covering the service.
Structured audit logging for every account-affecting operation.