Build an Open Banking Portfolio Project: Account Data, Consent and Failure Tests
By OpenFinanceTools
A useful portfolio project lets a reviewer run it, provoke a failure and understand why you handled that failure in a particular way. For open banking, an account-data explorer is a manageable starting point: it has permissions, external data, pagination and a clear distinction between a complete refresh and an error.
This guide starts with a downloadable local lab, then describes how to extend it into an application. The lab uses synthetic data and an in-memory transport. It does not connect to a bank, implement OAuth or establish FAPI conformance. Those boundaries make the first iteration reproducible without credentials and leave the provider integration as a separate, visible piece of work.
1. Write a small project brief
Build an application that displays synthetic account transactions after a permission check. It should retrieve all pages, validate the records and either return a complete result or a clear error. Its first version does not initiate payments, categorize spending or persist sensitive data.
Define three acceptance criteria before choosing a framework:
- An authorized request with two pages produces a complete list with identical duplicates removed.
- Inactive, expired or insufficient permission prevents a successful read.
- A malformed page, repeated cursor or upstream failure is visible as a failure rather than an apparently complete result.
These constraints create a coherent demonstration. A reviewer can understand them quickly and ask how you would change them for a real provider's contract. Keep a short decision log as you implement; the reasoning is part of the portfolio.
2. Run the dependency-free starter
Download account-data-lab.mjs. Save it in an empty project folder, inspect the file and run it with Node.js 22 or later:
node account-data-lab.mjs
The file performs no network calls and requires no package installation. Its clock is fixed so the expiry scenarios produce the same result whenever you run it. Successful execution ends with 10 checks passed; an assertion failure exits unsuccessfully.
The module exports checkConsent, readTransactions and runLab. Importing it does not automatically run the lab. This lets you reuse the reader in a separate test file or small application without copying its implementation.
Look at the first fixture. Its transaction has an identifier, an integer amountMinor and a three-letter currency. A negative amount represents an outflow in this exercise. These are our teaching conventions, not a standardized open banking response schema. The lab uses GBP examples and performs no currency conversion.
3. Understand the boundaries
The reader takes three important dependencies: the consent object, a clock function and a transport function. The transport receives an opaque cursor and returns a status and body. It is responsible for obtaining a page; the reader is responsible for accepting or rejecting that page.
Caller
→ permission check
→ transport(cursor)
→ permission recheck
→ response validation
→ duplicate handling
→ next page or complete result
Injecting a clock lets a test move time forward without sleeping. Injecting the transport lets it simulate a rate limit or malformed response without depending on an external service. Those seams are especially useful when you later add a provider adapter.
The reader rechecks local permission after each awaited request. That covers an expiry or an in-memory revocation while the request is pending. It is not a substitute for the provider's own authorization checks, nor does it implement distributed revocation. An application with multiple processes would need an authoritative permission source and a policy for changes during a refresh.
Pagination is bounded in two ways: a maximum page count and detection of a repeated cursor. Identical repeated transactions are deduplicated. Conflicting values for the same identifier cause an error. A real provider may legitimately update a transaction, so its adapter and application policy may need a different conflict strategy.
4. Make the failure cases explainable
| Included check | Expected outcome | What to explain |
|---|---|---|
| Two pages and an identical duplicate | Two unique records | What makes an identifier stable? |
| Expired consent | consent_expired | Why expiry at the exact boundary is rejected. |
| Revoked consent | consent_inactive | Where a real revocation decision comes from. |
| Missing permission | permission_missing | Why permission is specific to the operation. |
| 429 response | rate_limited | Who owns retry scheduling. |
| 401 response | authorization_failed | Why automatic repeated calls do not solve authorization. |
| Repeated cursor | pagination_loop | How traversal stays bounded. |
| Page limit reached | page_limit_exceeded | Why partial data is not returned as complete. |
| Fractional minor-unit amount | invalid_transaction | Which money representation the application accepts. |
| Conflicting duplicate | conflicting_duplicate | Whether the provider permits record updates. |
The errors are intentionally simple strings in this exercise. As you extend it, introduce typed errors or structured error results if they help callers distinguish recovery choices. Preserve the observable behavior with tests so changing the internal representation does not silently change the user experience.
For an additional exercise, make the clock advance during a transport call and verify that the result is rejected when permission expires. Then verify that invalid permission prevents the transport from being called at all. These tests check the boundaries rather than mirroring the loop line by line.
5. Build a small application around it
Add a page with a transaction list, a refresh button and a visible result state. Begin with three states: complete, loading and failed. Disable duplicate submissions or deliberately manage concurrency so a slower older response cannot overwrite a newer result.
Decide whether a previously successful snapshot remains visible when refresh fails. If it does, label it with the time of its last successful refresh and make the new failure visible. Never relabel an old snapshot as freshly fetched because the user clicked the button.
Keep business logic separate from display formatting. Use the JSON Formatter to inspect synthetic fixture payloads. If you introduce dates, document whether values are ISO timestamps or numeric seconds and use the Timestamp Converter to inspect examples.
A useful repository structure separates the reader, provider adapters, fixtures, tests and a short runbook. Include an example configuration file with placeholders, plus a README with exact commands. Add screenshots only after the executable behavior is easy to reproduce.
6. Add a provider sandbox as a separate milestone
Choose one provider and read its current sandbox and API documentation. For example, the TrueLayer Data API overview is a starting point for understanding that provider's data interfaces. Check access requirements and supported environments before committing to a demonstration.
Create an adapter that converts the provider's response into your application's model. Preserve cursor semantics, identifier guarantees and monetary precision deliberately. Document what happens to fields you omit. Keep captured fixtures synthetic or approved for public use.
The network adapter needs request timeouts, cancellation, bounded response handling and provider-specific error mapping. Keep endpoint configuration trusted; do not let arbitrary browser input select a backend fetch destination. These concerns sit outside the in-memory starter.
Implement authorization using the provider's documented registration and security profile. Keep credentials on the backend, correlate authorization transactions and protect stored tokens. Work through our FAPI guide and JWT failure lab for background, then test against the actual provider contract.
Continue running the local fixture tests even after the sandbox works. A sandbox success demonstrates one environment at one time; deterministic failure tests make regressions easier to diagnose. Record the tested API version and date in the integration notes.
7. Present the project in five minutes
- State the problem: retrieve account data with explicit permission and completeness rules.
- Show a successful run: two pages become one validated result.
- Trigger a failure: revoke permission or repeat a cursor and show the outcome.
- Explain one tradeoff: for example, rejecting conflicting duplicates instead of silently replacing them.
- State the boundaries: distinguish the local simulation, any tested sandbox integration and work still required for production.
Prepare a short answer for what you would improve next: persistent consent state, observability, background refresh or a second provider adapter. Pick the improvement that fits the role you are applying for. The role roadmap and interview exercises help you choose that emphasis.
When the project is reproducible and its limitations are clear, use the careers directory to research relevant teams. The strongest part of the demonstration is your ability to explain the contract, provoke a failure and show the test that protects the behavior.