Bootstrap your web application routing with ease.
With arrow-express
you can easily configure your backend application routes in a framework-agnostic way.
Out of the box it supports Express applications via the ExpressAdapter
, but the core library is designed to work with any web framework through custom adapters.
What arrow-express
offers:
- Framework-agnostic route definition system.
- Built-in Express adapter for seamless integration with Express applications.
- Quick way to define routes in applications.
- Define common routes properties by controllers.
- Define common prefix for routes grouped under controller.
- Define common context for routes grouped under controller. Eg: authorize user.
- Nest controllers.
- Quickly define route by chaining methods.
- Define method.
- Define path.
- Define handler.
- In handler have access to request, response and context from controller.
- Error handling.
- Throw
RequestError
to send back desired response with code.
- Throw
- TypeScript support with full type safety for request/response objects.
What arrow-express
doesn't offer:
- It's not a replacement for web frameworks like Express.
- It's not a complete backend framework.
- It won't take care of database connections.
- It won't take care of authorization (but provides tools to implement it).
- Et cetera.
Here's a complete example showing how to create a simple API with arrow-express
:
import Express from "express";
import { Application, Controller, Route, ExpressAdapter } from "arrow-express";
// 1. Create Express app
const app = Express();
// 2. Define your routes using arrow-express
const userController = Controller()
.prefix("users")
.handler(async (req, res) => {
// This runs before every route in this controller
// You can add authentication, logging, etc.
return { timestamp: new Date() };
})
.registerRoutes(
Route()
.method("get")
.path(":id")
.handler(async (req, res, context) => {
const userId = req.params.id;
return { id: userId, name: "John Doe", ...context };
}),
Route()
.method("get")
.path("")
.handler(async (req, res, context) => {
return { users: [{ id: 1, name: "John" }], ...context };
})
);
// 3. Register routes with Express
const application = Application().registerController(userController);
ExpressAdapter(app, application).configure();
// 4. Start server
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
This creates:
-
GET /users/:id
- Get user by ID -
GET /users
- Get all users
You can also use arrow-express
without Express to generate route configurations:
import { Application, Controller, Route } from "arrow-express";
const application = Application()
.prefix("api")
.registerController(
Controller()
.prefix("users")
.registerRoute(
Route()
.method("get")
.path(":id")
.handler(async (req, res) => ({ id: req.params.id }))
)
);
// Get route configurations for any framework
const routes = application.buildRoutes();
console.log(routes);
// Output: [{ path: 'api/users/:id', method: 'get', handler: Function }]
npm install arrow-express
For Express integration, you'll also need Express (if not already installed):
npm install express
npm install -D @types/express # For TypeScript projects
To get full type safety, configure TypeScript module augmentation:
// types.ts or at the top of your main file
import "arrow-express";
import { Request, Response } from "express";
declare module "arrow-express" {
namespace ArrowExpress {
interface InternalRequestType extends Request {}
interface InternalResponseType extends Response {}
}
}
This gives you proper typing for req
and res
parameters in your handlers.
The Application
is the root container for your routing configuration. It's framework-agnostic and doesn't interact with any specific web framework directly.
Key Features:
- Register controllers and their routes
- Set application-wide prefixes
- Build route configurations for any framework
- Compose multiple controllers into a single application
import { Application } from "arrow-express";
const app = Application()
.prefix("api/v1") // All routes will be prefixed with /api/v1
.registerController(userController)
.registerControllers(authController, adminController);
// Get all route configurations
const routes = app.buildRoutes();
Methods:
-
prefix(prefix: string)
- Set application-wide prefix for all routes -
registerController(controller)
- Register a single controller -
registerControllers(...controllers)
- Register multiple controllers -
buildRoutes()
- Generate route configurations for framework integration
Controllers group related routes under a common prefix and can share middleware-like handlers. They can be nested to create hierarchical route structures.
Key Features:
- Group routes with common prefix (e.g.,
/users
,/admin
) - Share context between routes (authentication, logging, etc.)
- Nest controllers for complex route hierarchies
- Chain handlers for middleware-like behavior
import { Controller, Route } from "arrow-express";
const userController = Controller()
.prefix("users")
.handler(async (req, res) => {
// This runs before every route in this controller
// Perfect for authentication, logging, validation, etc.
const user = await authenticateUser(req);
return { user, requestId: generateId() };
})
.registerRoute(
Route()
.method("get")
.path("profile")
.handler(async (req, res, context) => {
// context contains the result from controller handler
return { profile: context.user.profile };
})
)
.registerController(
// Nested controller: /users/admin/*
Controller()
.prefix("admin")
.handler(async (req, res, context) => {
// Can access parent context and add additional context
if (!context.user.isAdmin) {
throw new RequestError(403, { message: "Admin required" });
}
return { ...context, adminAccess: true };
})
.registerRoute(
Route()
.method("get")
.path("dashboard")
.handler(async (req, res, context) => {
return { dashboard: "admin data", user: context.user };
})
)
);
Methods:
-
prefix(prefix: string)
- Set URL prefix for all routes in this controller -
handler(handler)
- Set middleware function that runs before all routes -
registerRoute(route)
- Add a single route to this controller -
registerRoutes(...routes)
- Add multiple routes to this controller -
registerController(controller)
- Add a nested sub-controller -
registerControllers(...controllers)
- Add multiple nested controllers
Routes define individual endpoints with their HTTP method, path, and handler function.
Key Features:
- Support all HTTP methods (GET, POST, PUT, DELETE, etc.)
- Path parameters and query strings
- Access to request, response, and controller context
- Automatic response handling or manual response control
import { Route, RequestError } from "arrow-express";
const getUserRoute = Route()
.method("get")
.path(":id") // Path parameter
.handler(async (req, res, context) => {
const userId = req.params.id;
// You can return data (automatic 200 response)
if (userId === "me") {
return { user: context.user };
}
// Or manually control the response
if (!userId) {
res.status(400).json({ error: "User ID required" });
return; // Don't return data when manually responding
}
// Or throw errors for error handling
const user = await findUser(userId);
if (!user) {
throw new RequestError(404, { message: "User not found" });
}
return { user };
});
const createUserRoute = Route()
.method("post")
.path("")
.handler(async (req, res, context) => {
const userData = req.body;
const newUser = await createUser(userData);
// Set custom status code before returning
res.status(201);
return { user: newUser };
});
Methods:
-
method(method)
- Set HTTP method ("get", "post", "put", "delete", etc.) -
path(path)
- Set route path (supports Express-style parameters like:id
) -
handler(handler)
- Set the request handler function
Handler Function:
- Receives
(request, response, context)
parameters -
request
- HTTP request object (typed based on your framework) -
response
- HTTP response object (typed based on your framework) -
context
- Result from controller handlers (authentication data, etc.) - Can return data for automatic JSON response with 200 status
- Can manually use
response
object for custom responses - Can throw
RequestError
for automatic error responses
The ExpressAdapter
bridges arrow-express
route configurations with Express.js applications. It handles request/response processing, error handling, and route registration.
Key Features:
- Automatic route registration in Express
- Built-in error handling with
RequestError
- Request/response processing
- Route configuration logging
- Prevents double-configuration
import Express from "express";
import { Application, ExpressAdapter } from "arrow-express";
const app = Express();
const application = Application().registerController(userController);
// Configure Express with arrow-express routes
ExpressAdapter(app, application).configure();
// Start server
app.listen(3000);
Methods:
-
configure(printConfiguration = true)
- Registers all routes from the application into Express-
printConfiguration
- Whether to log registered routes to console (default: true) - Throws error if called multiple times (prevents duplicate route registration)
-
Error Handling:
The adapter automatically handles RequestError
exceptions:
// In your route handler
throw new RequestError(404, { message: "User not found", code: "USER_NOT_FOUND" });
// Results in HTTP 404 response with JSON body:
// { "message": "User not found", "code": "USER_NOT_FOUND" }
arrow-express
provides built-in error handling through the RequestError
class.
import { RequestError } from "arrow-express";
// In any route or controller handler
throw new RequestError(401, {
message: "Authentication required",
code: "AUTH_REQUIRED",
});
// Results in HTTP 401 response:
// { "message": "Authentication required", "code": "AUTH_REQUIRED" }
RequestError Constructor:
-
httpCode
(number) - HTTP status code (default: 500) -
response
(object) - JSON response body
Automatic Handling:
- When using
ExpressAdapter
,RequestError
exceptions are automatically caught - The HTTP status code and response body are automatically sent
- Other errors result in 500 Internal Server Error responses
Use function closures to inject dependencies and improve testability:
// services/userService.ts
export class UserService {
async getUser(id: string) {
/* ... */
}
async createUser(data: any) {
/* ... */
}
}
// controllers/userController.ts
export function UserController(userService: UserService) {
return Controller()
.prefix("users")
.registerRoutes(
Route()
.method("get")
.path(":id")
.handler(async (req, res) => {
const user = await userService.getUser(req.params.id);
return { user };
}),
Route()
.method("post")
.path("")
.handler(async (req, res) => {
const user = await userService.createUser(req.body);
res.status(201);
return { user };
})
);
}
// index.ts
const userService = new UserService();
const application = Application().registerController(UserController(userService));
Benefits:
- Easy unit testing without module mocking
- Clear dependency management
- No singleton dependencies
- Better separation of concerns
Implement authentication using controller handlers:
import { Controller, Route, RequestError } from "arrow-express";
function AuthController(authService: AuthService) {
return Controller()
.prefix("auth")
.handler(async (req, res) => {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) {
throw new RequestError(401, { message: "Authentication required" });
}
const user = await authService.verifyToken(token);
if (!user) {
throw new RequestError(401, { message: "Invalid token" });
}
return { user, authenticated: true };
})
.registerRoutes(
Route()
.method("get")
.path("profile")
.handler(async (req, res, context) => {
return { profile: context.user.profile };
}),
Route()
.method("put")
.path("profile")
.handler(async (req, res, context) => {
const updatedUser = await authService.updateProfile(context.user.id, req.body);
return { user: updatedUser };
})
);
}
Create complex route structures with nested controllers:
const apiController = Controller()
.prefix("api/v1")
.handler(async (req, res) => {
// Global API middleware (rate limiting, logging, etc.)
return { apiVersion: "v1", requestId: generateId() };
})
.registerControllers(
// /api/v1/users/*
Controller()
.prefix("users")
.handler(authenticateUser)
.registerControllers(
// /api/v1/users/admin/*
Controller()
.prefix("admin")
.handler(requireAdmin)
.registerRoute(Route().method("get").path("dashboard").handler(getAdminDashboard)),
// /api/v1/users/profile/*
Controller()
.prefix("profile")
.registerRoutes(
Route().method("get").path("").handler(getProfile),
Route().method("put").path("").handler(updateProfile)
)
),
// /api/v1/public/*
Controller()
.prefix("public")
.registerRoute(
Route()
.method("get")
.path("health")
.handler(async () => ({ status: "ok" }))
)
);
If you're upgrading from v3.x, here are the key changes:
- Express is now optional: Install Express separately if needed
-
ExpressAdapter is required: Use
ExpressAdapter(app, application).configure()
instead of direct Express integration - Framework-agnostic core: The core library no longer depends on Express
Before (v3.x):
import { Application } from "arrow-express";
// Express was a required dependency
After (v4.x):
import { Application, ExpressAdapter } from "arrow-express";
import Express from "express";
const app = Express();
const application = Application().registerController(controller);
ExpressAdapter(app, application).configure(); // New adapter pattern
Check out the example-express
and example-no-express
folders in the repository for complete working examples:
- example-express: Full Express.js integration with authentication, services, and controllers
- example-no-express: Framework-agnostic usage for generating route configurations
Contributions are welcome! Please read the contributing guidelines and submit pull requests to the GitHub repository.
MIT © Soldev - Tomasz Szarek