Routing with CabinJ
CabinJ's Router
provides a flexible and powerful way to handle HTTP requests by mapping URLs and HTTP methods to specific handlers. It uses a trie-based structure for efficient route matching, supporting static paths, path parameters, and wildcards.
Core Concepts
- Trie-based Matching: Routes are stored in a trie (prefix tree) for fast lookups.
- Path Segments:
- Static Segments: Exact matches (e.g.,
/users
,/products
). - Dynamic Segments (Parameters): Capture parts of the URL path (e.g.,
/users/:id
, where:id
becomes a request parameter). - Wildcard Segments: Match any sequence of characters at the end of a path (e.g.,
/files/*
).
- Static Segments: Exact matches (e.g.,
- HTTP Methods: Routes can be defined for specific HTTP methods (GET, POST, PUT, DELETE, etc.) or for all methods.
- Middleware: Middleware can be applied globally to all routes within a router or specifically to individual routes.
- Sub-Routers: Routers can be nested (mounted) to organize routes into logical groups.
Creating a Router
You can create a new router instance easily:
import com.cabin.express.router.Router;
Router mainRouter = new Router();
Defining Routes
The Router
class provides convenient methods for each HTTP verb:
Basic Routes
Router router = new Router();
// GET request to /hello
router.get("/hello", (req, res) -> {
res.send("Hello, World!");
});
// POST request to /submit-data
router.post("/submit-data", (req, res) -> {
// Process request body
String data = req.getBodyAsString();
res.json(Map.of("status", "success", "received", data));
});
// Other methods: put(), delete(), patch(), options()
router.put("/items/:id", (req, res) -> { /* ... */ });
router.delete("/items/:id", (req, res) -> { /* ... */ });
Routes with Path Parameters
Path parameters allow you to capture values from the URL. Parameters are prefixed with a colon (:
).
router.get("/users/:userId/orders/:orderId", (req, res) -> {
String userId = req.getParam("userId");
String orderId = req.getParam("orderId");
res.send("User ID: " + userId + ", Order ID: " + orderId);
});
The captured values are available via req.getParam("paramName")
.
Wildcard Routes
A wildcard *
can be used at the end of a path to match anything that follows.
router.get("/files/*", (req, res) -> {
// req.getPath() will give the full path matched by the wildcard
String filePath = req.getPath().substring("/files/".length()); // Example to get the part after /files/
res.send("Accessing file: " + filePath);
});
router.all()
The all()
method registers a handler for all HTTP methods for a given path.
router.all("/admin/*", (req, res) -> {
// This handler will be called for GET, POST, PUT, etc. to /admin/some/path
System.out.println("Admin area accessed: " + req.getMethod() + " " + req.getPath());
// Potentially call next.handle(req, res) if this is middleware-like
});
Middleware
Middleware functions can process requests before they reach the main handler or modify the response.
Global Middleware
Apply middleware to all routes defined within a router using router.use(Middleware)
.
// Example: A simple logging middleware
Middleware loggingMiddleware = (req, res, next) -> {
System.out.println("Request: " + req.getMethod() + " " + req.getPath());
next.handle(req, res); // Call the next middleware or handler
System.out.println("Response status: " + res.getStatusCode());
};
router.use(loggingMiddleware);
router.get("/home", (req, res) -> res.send("Welcome Home!"));
All requests handled by this router will first pass through loggingMiddleware
.
Route-Specific Middleware
You can also apply middleware to specific routes.
Middleware authMiddleware = (req, res, next) -> {
String token = req.getHeader("Authorization");
if (token != null && token.equals("secret-token")) {
next.handle(req, res);
} else {
res.setStatusCode(401).send("Unauthorized");
}
};
// Single middleware
router.get("/secure-data", authMiddleware, (req, res) -> {
res.send("This is secure data.");
});
// Multiple middlewares
List<Middleware> securityChecks = List.of(authMiddleware, anotherCheckMiddleware);
router.get("/super-secure-data", securityChecks, (req, res) -> {
res.send("This is super secure data.");
});
Sub-Routers (Mounting Routers)
You can organize your application by breaking it into smaller, manageable routers and then mounting them onto a parent router.
Mounting at a Path Prefix
Use router.use(String pathPrefix, Router childRouter)
to mount a childRouter
at a specific pathPrefix
.
Router mainRouter = new Router();
Router userRouter = new Router();
userRouter.get("/", (req, res) -> res.send("User index")); // Will match /users
userRouter.get("/:id", (req, res) -> { // Will match /users/:id
String userId = req.getParam("id");
res.send("Profile for user " + userId);
});
mainRouter.use("/users", userRouter); // Mount userRouter under /users
// Now, a GET request to /users will be handled by userRouter's "/" handler.
// A GET request to /users/123 will be handled by userRouter's "/:id" handler.
Mounting at the Root
You can also mount a router at the root of another router using router.use(Router childRouter)
. This is less common for top-level routers but can be useful for composing middleware or default handlers.
Router baseRouter = new Router();
Router featureRouter = new Router();
featureRouter.get("/feature1", (req, res) -> res.send("Feature 1"));
baseRouter.use(featureRouter); // All routes from featureRouter are added to baseRouter
// A GET request to /feature1 on baseRouter will be handled.
How it Works (Internals)
The Router
uses a RouterNode
class to build a trie structure.
- Each
RouterNode
can have static children (for exact path segments), a dynamic child (for parameters like:id
), and a wildcard child (*
). - Handlers and route-specific middleware are stored at the
RouterNode
corresponding to the end of a route path. - When a request comes in, the router traverses this trie based on the request path segments to find the matching
RouterNode
and its associated handler and middleware. - Path normalization ensures that paths like
/path
,/path/
, andpath
are treated consistently.
Using Routers with CabinServer
Typically, you create a main router and then pass it to your CabinServer
instance. The Router
itself implements the Middleware
interface, so it can be directly used by the server.
// In your main application setup:
CabinServer server = new ServerBuilder()
.setPort(8080)
.build();
Router appRouter = new Router();
// Define global middleware for the appRouter
appRouter.use((req, res, next) -> {
System.out.println("App-level middleware: " + req.getPath());
next.handle(req, res);
});
// Define routes on appRouter
appRouter.get("/", (req, res) -> res.send("Welcome to the Cabin!"));
// Create and mount sub-routers
Router apiRouter = new Router();
apiRouter.get("/version", (req, res) -> res.json(Map.of("version", "1.0.0")));
appRouter.use("/api", apiRouter);
// Use the main router with the server
server.use(appRouter);
server.start();
In this setup, CabinServer
will delegate incoming requests to appRouter
, which will then use its routing logic (including any mounted sub-routers like apiRouter
) to find and execute the appropriate handler and middleware chain.