Most backend frameworks organize routes by resource first:
/controllers/users.js → GET, POST, PUT, DELETE /users
/controllers/products.js → GET, POST, PUT, DELETE /products
I flipped this entirely. In BEnder, routes organize by HTTP method first:
/methods/GET/users/ → GET /users
/methods/POST/users/ → POST /users
Here's why this seemingly small change unlocks major architectural benefits.
The Problem with Resource-First Organization
When you group routes by resource, you inevitably mix concerns:
// controllers/users.js
class UsersController {
getAll() { /* read logic */ }
create() { /* write logic + validation + side effects */ }
delete() { /* destructive logic + auth checks */ }
}
These methods have vastly different concerns:
-
GETis idempotent, cacheable, safe -
POSTmutates state, triggers webhooks -
DELETErequires elevated permissions
Bundling them together makes sense from a resource perspective, but creates friction when:
- You want to apply different middleware per method
- You need to extract write operations to a separate microservice
- You're debugging a
POSTbug buried in a 500-line controller
The Neuron/Synapse Pattern
BEnder uses a brain-inspired architecture:
- Neurons 🧠 — Containers that auto-discover routes from the filesystem
- Synapses ⚡ — Endpoint handlers that process specific requests
methods/
├── GET/ ← Neuron: all read operations
│ ├── users/Users.ts ← Synapse: GET /users/:action
│ └── products/Prods.ts ← Synapse: GET /products/:action
├── POST/ ← Neuron: all write operations
│ └── users/Users.ts ← Synapse: POST /users/:action
└── DELETE/ ← Neuron: all destructive operations
└── users/Users.ts ← Synapse: DELETE /users/:action
Zero manual route registration. Drop a file in the right folder, export a class—done.
The Architecture
// A Synapse is an endpoint handler
export class GET_Users extends Synapse {
dir = __dirname;
protected async setRouter(): Promise<void> {
this.router.get('/:id', async (req, res) => {
const { code, data } = await this.tryer(async () => {
return await fetchUser(req.params.id);
});
this.responser(res, code, data);
});
}
}
Key features:
-
tryer<T>()— Async wrapper with automatic error logging -
responser()— Standardized response formatting (JSON, HTML, text, stream) -
readypromise — Ensures async route registration completes before server starts
Why Method-First Matters
1. Cleaner Middleware Application
// Apply read-caching only to GET Neurons
server.get(/.*/, cacheMiddleware, getRouter);
// Apply write-validation only to POST Neurons
server.post(/.*/, validateMiddleware, postRouter);
2. Microservice Extraction
Need to split reads and writes? Your folder structure already reflects that separation:
GET/ → Read replica service
POST/ → Write primary service
3. Cognitive Clarity
When debugging a POST /users/create bug, you go to:
methods/POST/users/Users.ts
Not a 500-line UsersController with 12 methods.
The apps/ Philosophy
Business logic lives outside the routing layer:
BEnder/
├── methods/ ← Thin HTTP routing layer (Neurons + Synapses)
└── apps/ ← Pure business logic (extractable, testable)
├── payments/
├── notifications/
└── analytics/
Each apps/ folder is:
- Importable without HTTP — spawn as child process or worker
- Extractable — move to its own microservice with copy-paste
- Testable — unit test pure functions, no Express mocking
// Testing business logic directly
import { calculateTotal } from './apps/payments/service';
test('calculates order total', () => {
expect(calculateTotal(items)).toBe(99.99);
});
Comparison with Popular Frameworks
| Feature | Rails | NestJS | Next.js | BEnder |
|---|---|---|---|---|
| Route organization | Resource-first | Resource-first | File-path | Method-first |
| Auto-discovery | ✅ | Partial | ✅ | ✅ |
| Microservice ready | ⚠️ Refactoring | ⚠️ Module extraction | ❌ | ✅ Native |
| Child process friendly | ❌ | ⚠️ | ⚠️ | ✅ |
| Unit test simplicity | ⚠️ | ⚠️ | ⚠️ | ✅ |
Getting Started
git clone https://github.com/Adam-Golan/BEnder.git my-backend
cd my-backend
npm install
npm run server
Create a new endpoint:
- Make a folder:
methods/GET/hello/ - Create
methods/GET/hello/Hello.ts:
import { Synapse } from '../../base';
import { Request, Response } from 'express';
export class GET_Hello extends Synapse {
dir = __dirname;
protected async setRouter(): Promise<void> {
this.router.get('/world', (req: Request, res: Response) => {
this.responser(res, 200, { message: 'Hello, World!' });
});
}
}
- Visit
http://localhost:3000/hello/world✅
When to Use BEnder
Good fit:
- API-first backends
- Projects likely to evolve into microservices
- Teams that think in HTTP verbs
- Codebases needing clear read/write separation
Maybe not:
- Simple CRUD apps where resource-grouping is intuitive
- Serverless-first deployments (use dedicated frameworks)
- Projects requiring heavy ORM integration out of the box
Conclusion
BEnder isn't trying to replace Express—it's a layer on top that enforces clean architecture through filesystem conventions.
The Neuron/Synapse pattern emerged from real frustration with tangled controllers and painful microservice extractions. If you've ever spent hours refactoring a monolith or debugging which middleware applies to which route, this approach might resonate.
Built with ❤️ for developers who believe backends deserve better architecture.
Top comments (0)