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 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.
 
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-expressFor Express integration, you'll also need Express (if not already installed):
npm install express
npm install -D @types/express  # For TypeScript projectsTo 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 responseobject for custom responses
- Can throw RequestErrorfor 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,RequestErrorexceptions 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 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 patternCheck 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