Vix.cpp v2.8.5 is here Read the blog
Skip to content

Routing ​

This page shows how routing works in Vix Core.

Use it when you want to register routes, use route parameters, group routes, handle OPTIONS, customize not-found behavior, and understand how the router matches requests.

Public header ​

cpp
#include <vix.hpp>

You can also include the router header directly:

cpp
#include <vix/router/Router.hpp>

What routing provides ​

Vix Core routing connects an HTTP method and path to a handler.

It provides:

  • method-based routes
  • static paths
  • route parameters
  • route groups
  • automatic OPTIONS routes
  • HEAD fallback to GET
  • custom not-found handling
  • route metadata
  • heavy route classification

Most applications register routes through vix::App.

cpp
vix::App app;

app.get("/", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.text("Hello from Vix");
});

Basic route ​

cpp
#include <vix.hpp>

int main()
{
  vix::App app;

  app.get("/", [](vix::Request &req, vix::Response &res)
  {
    (void)req;

    res.text("Home");
  });

  app.run(8080);

  return 0;
}

Run:

bash
vix run main.cpp

Then open:

text
http://localhost:8080

Expected response:

text
Home

HTTP methods ​

App provides helpers for common HTTP methods.

cpp
app.get("/users", handler);
app.post("/users", handler);
app.put("/users/{id}", handler);
app.patch("/users/{id}", handler);
app.del("/users/{id}", handler);
app.head("/users", handler);
app.options("/users", handler);

Example:

cpp
#include <vix.hpp>

int main()
{
  vix::App app;

  app.get("/users", [](vix::Request &req, vix::Response &res)
  {
    (void)req;

    res.json({
      "method", "GET",
      "route", "/users"
    });
  });

  app.post("/users", [](vix::Request &req, vix::Response &res)
  {
    (void)req;

    res.status(201).json({
      "method", "POST",
      "created", true
    });
  });

  app.run(8080);

  return 0;
}

Route handlers ​

A route handler receives:

cpp
vix::Request &req
vix::Response &res

Example:

cpp
app.get("/status", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.json({
    "status", "ok"
  });
});

The handler reads from the request and writes to the response.

Static paths ​

A static route matches an exact path.

cpp
app.get("/about", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.text("About");
});

This matches:

text
GET /about

It does not match:

text
GET /about/team

Route parameters ​

Use {name} to capture a path segment.

cpp
app.get("/users/{id}", [](vix::Request &req, vix::Response &res)
{
  const std::string id = req.param("id");

  res.json({
    "id", id
  });
});

Request:

text
GET /users/42

Example response:

json
{
  "id": "42"
}

Multiple parameters ​

A route can contain several parameters.

cpp
app.get("/users/{user_id}/posts/{post_id}", [](vix::Request &req, vix::Response &res)
{
  res.json({
    "user_id", req.param("user_id"),
    "post_id", req.param("post_id")
  });
});

Request:

text
GET /users/7/posts/42

Example response:

json
{
  "user_id": "7",
  "post_id": "42"
}

Query string ​

The router matches the path without the query string.

text
GET /users/42?page=2

matches:

cpp
app.get("/users/{id}", handler);

You can read query values from the request.

cpp
app.get("/users/{id}", [](vix::Request &req, vix::Response &res)
{
  res.json({
    "id", req.param("id"),
    "page", req.query_value("page", "1")
  });
});

Path normalization ​

Routes are normalized before they are stored.

These paths are treated cleanly:

cpp
app.get("users", handler);
app.get("/users", handler);
app.get("/users/", handler);

The normalized route path is:

text
/users

Method normalization ​

HTTP methods are normalized to uppercase.

cpp
app.get("/status", handler);

internally registers:

text
GET /status

The router also normalizes request methods before matching.

Route groups ​

Use group(...) to register many routes under the same prefix.

cpp
#include <vix.hpp>

int main()
{
  vix::App app;

  app.group("/api", [](auto &api)
  {
    api.get("/status", [](vix::Request &req, vix::Response &res)
    {
      (void)req;

      res.json({"status", "ok"});
    });

    api.get("/version", [](vix::Request &req, vix::Response &res)
    {
      (void)req;

      res.json({"version", "1.0.0"});
    });
  });

  app.run(8080);

  return 0;
}

The routes become:

text
GET /api/status
GET /api/version

Nested route groups ​

Groups can be nested.

cpp
app.group("/api", [](auto &api)
{
  api.group("/v1", [](auto &v1)
  {
    v1.get("/status", [](vix::Request &req, vix::Response &res)
    {
      (void)req;

      res.json({
        "status", "ok"
      });
    });
  });
});

The route becomes:

text
GET /api/v1/status

Reusable group object ​

You can create a group object and reuse it.

cpp
auto api = app.group("/api");

api.get("/status", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.json({"status", "ok"});
});

api.post("/users", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.status(201).json({"created", true});
});

Group middleware ​

A group can register middleware for its prefix.

cpp
app.group("/admin", [](auto &admin)
{
  admin.use([](vix::Request &req, vix::Response &res, vix::App::Next next)
  {
    const bool allowed = true;

    if (!allowed)
    {
      res.status(403).text("Forbidden");
      return;
    }

    next();
  });

  admin.get("/dashboard", [](vix::Request &req, vix::Response &res)
  {
    (void)req;

    res.text("Admin dashboard");
  });
});

The middleware applies to routes under:

text
/admin

Protect a route prefix ​

Use protect(...) to attach middleware to a route prefix.

cpp
app.protect("/admin", [](vix::Request &req, vix::Response &res, vix::App::Next next)
{
  const std::string auth = req.header("Authorization");

  if (auth.empty())
  {
    res.status(401).json({
      "error", "missing authorization header"
    });
    return;
  }

  next();
});

This applies to:

text
/admin
/admin/users
/admin/settings

Protect an exact path ​

Use protect_exact(...) when the middleware should only run for one exact path.

cpp
app.protect_exact("/admin/settings", [](vix::Request &req, vix::Response &res, vix::App::Next next)
{
  (void)req;

  const bool allowed = true;

  if (!allowed)
  {
    res.status(403).text("Forbidden");
    return;
  }

  next();
});

This applies only to:

text
/admin/settings

HEAD routes ​

You can register a HEAD route directly.

cpp
app.head("/status", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.status(200).send();
});

If a HEAD route is not found, the router can fall back to the matching GET route.

cpp
app.get("/status", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.json({"status", "ok"});
});

A HEAD /status request can reuse the GET /status handler, then finalize the response without a body.

OPTIONS routes ​

You can register an OPTIONS route manually.

cpp
app.options("/users", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.status(204).send();
});

When you register a normal route through App, Vix can also ensure an OPTIONS route exists for that path.

This helps clients and tooling perform preflight or capability checks.

Automatic 404 ​

If no route matches, Vix returns a default not-found response.

Example response shape:

json
{
  "error": "Route not found",
  "hint": "Check path, method, or API version",
  "method": "GET",
  "path": "/missing"
}

The application-level not-found handler can also try static files before returning the default 404.

Static files as fallback ​

Static files are integrated with routing through the not-found path.

cpp
app.static_dir("public", "/assets");

If no explicit route matches:

text
GET /assets/app.css

Vix can try to serve:

text
public/app.css

If the file is not found and fallthrough is enabled, routing continues to the default not-found response.

Heavy routes ​

Use heavy route helpers for expensive routes.

cpp
app.get_heavy("/reports", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.json({
    "report", "ready"
  });
});

For POST:

cpp
app.post_heavy("/reports", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.status(202).json({
    "accepted", true
  });
});

Heavy routes are stored with route metadata.

This lets Vix classify routes that may be CPU-heavy, database-heavy, or more expensive than normal request handlers.

Route metadata ​

The router stores metadata for registered routes.

A route record contains:

text
method
path
heavy
doc

This metadata is useful for:

  • documentation
  • OpenAPI generation
  • runtime checks
  • heavy route classification
  • developer tooling

Route documentation ​

Routes can carry documentation metadata through RouteDoc.

A route doc can contain:

  • summary
  • description
  • tags
  • request body metadata
  • response metadata
  • OpenAPI extensions

At the app level, most users do not need to construct RouteDoc directly.

It is used by tooling and documentation generation.

Internal route tree ​

Internally, the router stores routes in a tree.

text
Router
  -> root RouteNode
  -> children
  -> handler
  -> parameter nodes

A RouteNode can contain:

text
children
handler
isParam
paramName
heavy

Static segments are stored by their exact name.

Parameter segments such as {id} are stored using a parameter child.

Matching order ​

For each segment, the router tries:

text
static child
parameter child
not found

Example route:

text
GET /users/{id}

Request:

text
GET /users/42

Simplified matching:

text
GET/users/42
  -> GET
  -> users
  -> parameter id = 42
  -> handler

Router API ​

Advanced code can access the router.

cpp
auto router = app.router();

Most application code should register routes through App.

Direct router access is useful for:

  • tooling
  • integrations
  • generated routes
  • documentation systems
  • custom frameworks on top of Vix

Register directly on Router ​

Advanced usage:

cpp
#include <vix.hpp>
#include <memory>

int main()
{
  vix::App app;

  auto router = app.router();

  auto handler = std::make_shared<vix::http::RequestHandler<decltype(
      [](vix::Request &req, vix::Response &res)
      {
        (void)req;
        res.text("direct");
      })>>(
      "/direct",
      [](vix::Request &req, vix::Response &res)
      {
        (void)req;
        res.text("direct");
      });

  router->add_route("GET", "/direct", handler);

  app.run(8080);

  return 0;
}

In normal code, prefer:

cpp
app.get("/direct", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.text("direct");
});

Check if a route exists ​

The router can check whether a route exists.

cpp
if (app.router()->has_route("GET", "/status"))
{
  vix::print("route exists");
}

This is useful for tooling and route registration checks.

List registered routes ​

The router exposes the list of registered route records.

cpp
for (const auto &route : app.router()->routes())
{
  vix::print(route.method, route.path);
}

This can be used by documentation tools or diagnostics.

Routing lifecycle ​

The route lifecycle is:

text
App registers route
  -> middleware chain is collected
  -> user handler is wrapped
  -> RequestHandler is created
  -> Router stores route in tree
  -> Session dispatches Request
  -> Router matches method + path
  -> RequestHandler runs
  -> Response is finalized

Complete example ​

cpp
#include <vix.hpp>

int main()
{
  vix::App app;

  app.use([](vix::Request &req, vix::Response &res, vix::App::Next next)
  {
    (void)res;

    vix::print(req.method(), req.path());

    next();
  });

  app.get("/", [](vix::Request &req, vix::Response &res)
  {
    (void)req;

    res.text("Home");
  });

  app.get("/users/{id}", [](vix::Request &req, vix::Response &res)
  {
    res.json({
      "id", req.param("id")
    });
  });

  app.group("/api", [](auto &api)
  {
    api.get("/status", [](vix::Request &req, vix::Response &res)
    {
      (void)req;

      res.json({
        "status", "ok"
      });
    });
  });

  app.get_heavy("/reports", [](vix::Request &req, vix::Response &res)
  {
    (void)req;

    res.json({
      "report", "ready"
    });
  });

  app.static_dir("public", "/assets");

  app.run(8080);

  return 0;
}

Registered routes:

text
GET /.
GET /users/{id}
GET /api/status
GET /reports

Static fallback:

text
/assets/*

API summary ​

APIPurpose
app.get(path, handler)Register a GET route.
app.post(path, handler)Register a POST route.
app.put(path, handler)Register a PUT route.
app.patch(path, handler)Register a PATCH route.
app.del(path, handler)Register a DELETE route.
app.head(path, handler)Register a HEAD route.
app.options(path, handler)Register an OPTIONS route.
app.get_heavy(path, handler)Register a heavy GET route.
app.post_heavy(path, handler)Register a heavy POST route.
app.group(prefix, fn)Register routes under a prefix.
app.use(middleware)Register global middleware.
app.use(prefix, middleware)Register prefix middleware.
app.protect(prefix, middleware)Protect a prefix.
app.protect_exact(path, middleware)Protect one exact path.
app.router()Access the shared router.
router->has_route(method, path)Check if a route exists.
router->routes()Read registered route metadata.

Best practices ​

Keep route handlers small.

cpp
app.get("/status", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.json({"status", "ok"});
});

Use route groups for APIs.

cpp
app.group("/api", [](auto &api)
{
  api.get("/status", handler);
});

Use route parameters for resource identifiers.

cpp
app.get("/users/{id}", handler);

Use middleware for cross-cutting logic.

cpp
app.use("/admin", auth_middleware);

Use heavy routes for costly work.

cpp
app.get_heavy("/reports", handler);

Next steps ​

Read the next pages:

Released under the MIT License.