Skip to content

Built for Teams

Deserve keeps the structure of an app obvious, so a team reads the folder tree and already knows the API. There is no central route table to study and no framework-specific wiring to learn first. This follows the philosophy that file structure is the API structure, which keeps a codebase easy for teams to maintain.

The Folder Is the Map

A new contributor opens the routes folder and reads the endpoints straight from the paths:

routes/
├── index.ts            # GET  /
├── health.ts           # GET  /health
├── users/
│   ├── index.ts        # GET  /users
│   ├── [id].ts         # GET  /users/:id
│   └── [id]/
│       └── posts.ts    # GET  /users/:id/posts
└── orders/
    └── index.ts        # POST /orders

No registry to cross-check, no decorators to trace. The path on disk is the path on the wire, covered in File-based Routing.

The folder is the map: each file path maps directly to a URL pattern, so routes/index.ts becomes GET /, routes/users/index.ts becomes GET /users, routes/users/[id].ts becomes GET /users/:id, and files prefixed with an underscore are skipped as private

A Junior Ships on Day One

Adding an endpoint means adding a file. A junior developer who needs a GET /products route creates routes/products/index.ts and exports a handler:

typescript
// routes/products/index.ts
import type { 
Context
} from '@neabyte/deserve'
// New endpoint, no registration needed export function
GET
(
ctx
:
Context
): Response {
return
ctx
.
send
.
json
({
products
: []
}) }

The route is live on the next save through Hot Reload, with no restart and no edit to a shared config file that might cause a merge conflict.

A junior ships on day one: creating routes/products/index.ts triggers the watcher's file-created event, the module is imported and its GET handler registered, and the route answers GET /products on the next request, with no restart and no shared config edit so there is no merge conflict

Predictable Handlers

Every route file follows the same shape, so reviewing a teammate's code needs no guesswork. The exported function name is the HTTP method, and the Context gives the request and the response helpers:

typescript
// routes/orders/index.ts
import type { 
Context
} from '@neabyte/deserve'
// Method name is the HTTP verb export async function
POST
(
ctx
:
Context
):
Promise
<Response> {
// Read parsed JSON body const
order
= await
ctx
.
get
.
body
()
return
ctx
.
send
.
json
(
{
created
: true,
order
}, {
status
: 201 }
) }

A reviewer reads POST and knows the verb, reads ctx.get.body() and knows the input, reads ctx.send.json() and knows the output. The same pattern holds across every file, which is the developer experience the framework aims for. Details live in Request Handling and the Context Object.

Shared Rules in One Place

Cross-cutting concerns stay in one spot rather than scattered through handlers. One developer can own auth, another can own logging, and neither has to touch the other's route files:

typescript
// main.ts
import { 
Mware
,
Router
} from '@neabyte/deserve'
const
router
= new
Router
()
// Security headers for every route
router
.
use
(
Mware
.
securityHeaders
())
// Auth only for the admin area
router
.
use
(
'/admin',
Mware
.
basicAuth
({
users
: [
{
username
: 'admin',
password
: Deno.
env
.
get
('ADMIN_PASSWORD') ?? 'change-me'
} ] }) ) await
router
.
serve
(8000)

Handlers stay focused on their own job, while shared behavior is applied once. The full set of building blocks is in Global Middleware, and errors flow to one place through error handling. Input rules belong here too, where a validation contract checks a request before the handler so each route reads only data that already passed.

Shared rules in one place: securityHeaders() registered with router.use(fn) reaches every route, while basicAuth() registered with router.use('/admin', fn) reaches only /admin/*, so one developer can own auth and another own logging without touching each other's route files

Many Hands, One Process

Larger teams often split an app into services. Deserve runs several routers in a single process, so one person can work on the API while another works on auth without separate deployments or network glue between them:

typescript
// main.ts
import { 
Router
} from '@neabyte/deserve'
const
api
= new
Router
({
routes
: {
directory
: './services/api/routes' }
}) const
auth
= new
Router
({
routes
: {
directory
: './services/auth/routes' }
}) // Each service owns its folder and port await
Promise
.
all
([
api
.
serve
(3001),
auth
.
serve
(3002)
])

Each service has its own folder, port, and file watcher, so teams move in parallel without stepping on each other. The full pattern, including shared code and a shared error handler, is in Multi-Service.

Many hands, one process: a single Deno process runs an API router owned by dev A on port 3001 and an Auth router owned by dev B on port 3002, each with its own routes directory and file watcher, so the two developers work in parallel without separate deployments or network glue

Where to Go Next

Released under the MIT License.