BFF Architecture – Optimizing Communication between Node. js and Ext JS
Get a summary of this article:
Modern enterprise applications rarely struggle because they lack features. More often, they struggle because the frontend and backend are speaking slightly different languages.
- Your database is optimized for storage.
- Your API is often optimized for generic reuse.
But your UI – especially a rich, data-driven Ext JS application – is optimized for speed, structure, and interaction.
That mismatch creates friction.
The solution is not to make the frontend do more work. The solution is to introduce a smarter layer in the middle: a Backend-for-Frontend (BFF).
In this article, we’ll explore how to build a high-performance BFF with AdonisJS and Sencha Ext JS, and why this architecture can dramatically improve development speed, maintainability, and user experience.
Why a BFF Matters in Enterprise Applications
In enterprise systems, the frontend often needs data in a very specific shape.
An Ext JS grid, form, or store doesn’t just want raw rows from a database. It expects structured responses that support pagination, filtering, sorting, related data, and consistent success/error handling. If your backend sends “close enough” JSON, the frontend ends up spending time transforming payloads, stitching requests together, and compensating for backend inconsistencies.
That is wasted effort.
A BFF acts as a dedicated translation layer between the user interface and your backend services or databases. Its purpose is simple: serve the frontend exactly what it needs in the format it expects.
For Ext JS, that often means returning a response envelope like this:
{
"data": [],
"total": 0,
"success": true
}
That structure may look small, but it carries huge architectural value.
With a proper BFF, you can:
- reduce frontend transformation logic
- centralize filtering and sorting behavior
- simplify client-side stores
- lower latency by consolidating requests
- create a consistent contract for every screen in the application
Instead of pushing complexity into the browser, the BFF absorbs it where it belongs: in a controlled backend layer.
Why AdonisJS Is a Strong Fit
For this kind of architecture, I strongly favor AdonisJS.
If you’ve worked with Laravel and appreciated its structure, conventions, and developer ergonomics, AdonisJS feels immediately familiar. It brings that same “batteries included” philosophy to Node.js, making it a great choice for teams building serious APIs with TypeScript.
A BFF needs more than just routing and database access. It needs consistency. It needs opinionated structure. It needs patterns that scale across dozens or hundreds of endpoints.
That’s where AdonisJS shines.
Using Lucid ORM, middleware, validation, and TypeScript together gives you a backend that feels deliberate rather than improvised. And when you’re building a translation layer for a framework as capable as Ext JS, that discipline matters.
The Foundation: A Shared Base Model
The first real step toward a maintainable BFF is not the controller. It’s the base model.
A shared base model in AdonisJS gives you a single place to solve repeated problems that otherwise become scattered across your codebase.
1. Naming Strategy Without Friction
One of the oldest full-stack annoyances is the mismatch between:
- JavaScript’s camelCase
- database snake_case
If this is not handled centrally, it becomes a constant source of small bugs, awkward mappings, and inconsistent conventions.
A solid base model enforces a naming strategy that keeps the database and application aligned without forcing developers to think about it every time they define a field.
2. Dynamic Multitenancy
Many enterprise systems support multiple tenants, business units, or accounts. In those environments, selecting the correct database connection dynamically is critical.
A shared base model can encapsulate logic like:
- selecting the tenant connection
- switching data context based on the authenticated user
- keeping tenant resolution out of business-specific controllers
This keeps multitenancy from becoming repetitive boilerplate.
3. Field-Level Governance
A mature backend should be explicit about what data can be written, searched, and cloned.
That’s where properties like these become extremely useful:
- fillable: fields accepted during create/update operations
- searchable: fields allowed in global or dynamic search
- clonable: fields copied when duplicating records
This is more than neat architecture. It directly protects the system.
For example, fillable guards against accidental or malicious mass assignment. If a request includes fields that should never be writable, the backend simply ignores them.
Likewise, clonable can unlock real business value. Imagine duplicating a “Bill to Pay” record while preserving vendor, category, and description, but allowing a new amount or due date. That turns a tedious workflow into a one-click action.
The Magic Controller: Translating Ext JS Requests into SQL
This is where the BFF becomes truly powerful.
Ext JS stores send rich request payloads that often include:
- filters
- sorters
- pagination parameters like start and limit
- relationship expectations
A generic controller can’t handle that elegantly at scale. A dedicated Ext JS controller base class can.
Think of this as the “magic controller” – a reusable controller that understands the structure of Ext JS requests and knows how to translate them into efficient Lucid queries.
Parsing Frontend Intent
At runtime, the controller reads the incoming request and extracts:
- filters
- sorters
- limit
- start
This turns frontend interaction into backend instructions.
When the user sorts a grid by supplier name, applies a date range filter, and scrolls to the next page, the BFF understands exactly what happened and builds the correct query.
Flexible Filter Translation
The heart of the translation layer is often a method like applyFiltersToQuery.
A switch-based filter parser allows the backend to map Ext JS filter operators directly into SQL logic:
switch (filter.operator) {
case 'like':
query.where(filter.property, 'like', `%${filter.value}%`);
break;
case 'in':
query.whereIn(filter.property, filter.value);
break;
case 'notIn':
query.whereNotIn(filter.property, filter.value);
break;
case 'between':
query.whereBetween(filter.property, filter.value);
break;
case 'null':
query.whereNull(filter.property);
break;
case 'notNull':
query.whereNotNull(filter.property);
break;
case '<':
case '>':
query.where(filter.property, filter.operator, filter.value);
break;
default:
query.where(filter.property, '=', filter.value);
}
This is where the BFF stops being a thin wrapper and starts becoming a real asset.
Instead of writing custom filter logic for every entity, you build it once and reuse it everywhere.
Solving the N+1 Problem with Eager Loading
Performance is not just about fast queries. It’s about avoiding unnecessary queries.
One of the most common backend performance mistakes is the N+1 query problem. You fetch a list of records, and then for each record you fetch related data separately. What should have been one or two queries becomes dozens or hundreds.
In a BFF serving Ext JS, that kind of inefficiency is especially dangerous because rich grids and nested views often need relational context.
The fix is straightforward: use eager loading strategically.
With methods such as:
- applyBelongsToQuery
- applyBelongsToManyQuery
you can centralize how related data is loaded and ensure that nested relationships are fetched in optimized batches using Lucid’s relationship loading capabilities.
That means the frontend receives complete, UI-friendly data without triggering an avalanche of database calls.
CRUD at Speed Through Inheritance
One of my favorite advantages of this architecture is how quickly it scales.
Once your base Ext JS controller is built, creating a full-featured controller for a new entity can be almost trivial.
For example:
export default class SupplierController extends ExtJSController {
protected model = Supplier;
}
That’s it.
By extending the base controller and pointing it to the model, the new endpoint can instantly inherit:
- list operations
- record retrieval
- create/update logic
- delete functionality
- pagination support
- filter parsing
- sorting behavior
- duplication/cloning features
This is a major productivity win.
In enterprise delivery, speed does not come from cutting corners. It comes from building reusable patterns that eliminate repeated work.
Securing the Pipeline End to End
A BFF becomes the central nervous system of your frontend architecture, so security cannot be an afterthought.
Server-Side Protection with JWT
At the API layer, AdonisJS middleware should protect every authenticated route. If a request does not include a valid token, the backend should reject it immediately with a 401 Unauthorized response.
That gives you a clean and predictable security boundary.
Client-Side Interception in Ext JS
On the frontend, every request should automatically include the JWT in the Authorization header. This is best handled by a shared request interceptor rather than repeating token logic across stores and services.
That interceptor becomes responsible for attaching credentials consistently and invisibly.
Handling Expired Sessions Gracefully
One of the most frustrating UX failures in enterprise apps happens when the token expires but the interface remains on screen. The user sees dashboards, grids, and buttons – but every action silently fails.
That creates what I call a broken UI state.
A better approach is to intercept 401 responses globally and trigger a controlled logout flow:
- clear the local session
- destroy stale viewports or app shells
- redirect the user to login
- reset the application state cleanly
This keeps the application trustworthy. When the session is invalid, the UI should reflect that immediately.
Performance Strategy: What Stays on the Server vs. the Browser
High-performance enterprise UI is not about doing everything on the client or everything on the server. It’s about choosing correctly.
Remote Operations for Large Datasets
For operational datasets like:
- customers
- invoices
- suppliers
- products
- payments
- logs
you want remote filtering, remote sorting, and buffered scrolling.
Why?
Because browsers are fast, but browser memory is finite. Loading thousands of records into a grid and sorting them locally may work in a demo. It becomes a liability in production.
With buffered scrolling, the UI only renders a small subset of rows at a time – say 25 or 50. The BFF uses start and limit to fetch exactly the required slice.
This keeps the application responsive and memory-efficient.
Local Operations for Small Static Data
Client-side operations still have a place.
If you have a combo box with 10 or 15 static values, local sorting is usually faster and simpler than making a network request. The key is not ideological consistency. The key is practical performance.
Use the server for big, dynamic, operational data.
Use the browser for small, stable reference sets.
That balance is what keeps the app feeling fast.
A Postman-First Testing Workflow
One of the smartest habits in BFF development is testing the translator layer before the UI is built.
This is where a Postman-first workflow is incredibly effective.
Step 1: Capture Real Ext JS Payloads
Open the browser’s Network tab and interact with an Ext JS grid. Apply filters. Change sorting. Scroll through data. Then inspect the exact payload being sent.
This gives you real-world request structures rather than guessed examples.
Step 2: Replay Them in Postman
Copy those JSON payloads into Postman and hit your AdonisJS endpoints directly.
Now you can test:
- filter parsing
- sorting translation
- pagination behavior
- relationship loading
- edge cases for operators like between, in, or null
Step 3: Verify the Response Envelope
Before the frontend team ever binds a store to the endpoint, verify that the response includes the expected structure:
{
"data": [...],
"total": 245,
"success": true
}
This is a deceptively powerful workflow because it isolates the BFF’s core responsibility: translation. If the translator works correctly in Postman, the frontend integration becomes dramatically smoother.
Why This Architecture Works
When done right, the AdonisJS + Ext JS BFF pattern delivers several advantages at once:
- faster frontend development
- cleaner API contracts
- better performance for data-heavy interfaces
- less duplication across controllers
- safer write operations
- stronger multitenant control
- more graceful session handling
- easier testing and debugging
It also creates a healthier separation of concerns.
The frontend focuses on user interaction and presentation.
The BFF focuses on shaping and securing data.
The database focuses on persistence.
That is the kind of boundary definition that makes complex systems easier to evolve.
Final Thoughts
Enterprise applications need more than an API. They need an API layer that understands the frontend it serves.
That is the real value of a BFF.
By combining AdonisJS for structure and backend productivity with Ext JS for rich, data-intensive UI, you get an architecture that is both highly performant and highly maintainable. The shared base model creates consistency. The magic controller automates translation. Inheritance accelerates CRUD delivery. JWT interception secures the full request lifecycle. And a Postman-first verification strategy keeps the system predictable from day one.
If your Ext JS frontend is doing too much work to adapt generic backend responses, it may be time to stop stretching the client and start empowering the middle layer.
A well-designed BFF does not just connect frontend and backend.
It makes them work like they were designed for each other.
In enterprise software, responsiveness is no longer a nice-to-have. Users expect the same application to…
Modern teams are under constant pressure to ship faster without sacrificing UX quality, maintainability, or…
Hiring the right talent requires more than collecting resumes. Modern recruitment teams need streamlined workflows,…