arrow-express
v4.0.0
Published
Library to bootstrap express applications with zero configuration
Readme
Arrow Express
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.
Features
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
RequestErrorto 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.
Quick Start
Basic Example with Express
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 IDGET /users- Get all users
Framework-Agnostic Usage
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 }]Installation
npm install arrow-expressFor Express integration, you'll also need Express (if not already installed):
npm install express
npm install -D @types/express # For TypeScript projectsTypeScript Configuration
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.
Core Concepts
Application
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 routesregisterController(controller)- Register a single controllerregisterControllers(...controllers)- Register multiple controllersbuildRoutes()- Generate route configurations for framework integration
Controller
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 controllerhandler(handler)- Set middleware function that runs before all routesregisterRoute(route)- Add a single route to this controllerregisterRoutes(...routes)- Add multiple routes to this controllerregisterController(controller)- Add a nested sub-controllerregisterControllers(...controllers)- Add multiple nested controllers
Route
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
responseobject for custom responses - Can throw
RequestErrorfor automatic error responses
ExpressAdapter
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 ExpressprintConfiguration- 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" }Error Handling
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,RequestErrorexceptions are automatically caught - The HTTP status code and response body are automatically sent
- Other errors result in 500 Internal Server Error responses
Advanced Patterns
Dependency Injection with Closures
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
Authentication & Authorization
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 };
})
);
}Nested Route Hierarchies
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" }))
)
);Migration from v3.x
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 dependencyAfter (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 patternExamples
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
Contributing
Contributions are welcome! Please read the contributing guidelines and submit pull requests to the GitHub repository.
License
MIT © Soldev - Tomasz Szarek
