Routing is one of Ingest’s strongest ideas because the framework supports several route-definition styles without changing the handler model. You can change where a route comes from without changing how the route behaves once it runs.
No matter how a route is declared, the useful target is still the same props-based action:
({ req, res, ctx }) => {
res.setJSON({ path: req.url.pathname });
}That consistency makes the routing modes feel like variations of one system instead of unrelated features. It prevents the framework from drifting into several incompatible route APIs as the project scales.
Use inline routes when the handler is short and local readability matters most.
app.get('/users/:id', ({ req, res }) => {
res.setJSON({ id: req.data('id') });
});Inline routes solve the straightforward case without requiring a file, import boundary, or plugin when the behavior is small and local.
Use entry routes when you want route ownership to map directly to files.
app.entry.get('/users/:id', './routes/user.js');// ./routes/user.js
export default function UserDetail({ req, res }) {
res.setJSON({ id: req.data('id') });
}This pattern is useful when the filesystem is part of how the app is organized or built. It keeps route ownership explicit, reduces the chance that one registration file turns into a large aggregation point, and gives build tooling a stable route-to-file map.
Use import routes when handlers should load on demand or when tooling needs to see import boundaries directly.
app.import.get('/users', () => import('./routes/users.js'));// ./routes/users.js
export default function UsersIndex({ res }) {
res.setResults([
{ id: 1, name: 'Ada' },
{ id: 2, name: 'Grace' }
]);
}This pattern matters for:
- lazy loading
- server build scripts
- deployment packaging
- route-aware tooling
Import routes solve more than load timing. They make route module boundaries visible to tooling so large projects can package, inspect, or deploy them more deliberately.
console.log(app.imports);
// [
// ['GET /users', [{ priority: 0 }]]
// ]Attach a template engine and use view routes to automatically render template files.
app.view.engine = async (filePath, req, res) => {
const html = await renderTemplate(filePath, req.data());
res.setHTML(html);
};
app.view.get('/profile', './views/profile.hbs');View routes keep template lookup connected to routing so simple rendered pages do not need to repeat the same rendering boilerplate in every handler.
Use decorated controllers when you want class-based organization without changing the underlying router behavior.
import {
Controller,
Get,
server,
type HttpAction
} from '@stackpress/ingest/http';
type HttpProps = Parameters<HttpAction>[0];
@Controller('/api')
class UserController {
@Get('/users')
public list({ res }: HttpProps) {
res.setJSON([{ id: 1, name: 'Ada' }]);
}
}
const app = server();
app.mount(UserController);This pattern is useful when:
- you want related routes grouped on a class
- you want route registration sugar without a dependency injection container
- you still want explicit control over which controllers become active
Decorators are optional in Ingest. They only write route and event metadata that mount() later registers through the same route() and on() APIs used by manual routing.
Underneath these styles, Ingest routes are still regular router entries. That means you keep the same matching features, route parameters, wildcards, and router composition regardless of the route source.
app.post('/users/:id', ({ req, res }) => {
res.setJSON({ id: req.data('id') });
});
app.put('/files/*', ({ req, res }) => {
res.setJSON({ args: req.data() });
});
app.get('/assets/**', ({ req, res }) => {
res.setJSON({ args: req.data() });
});The matching behavior comes from ExpressEmitter, which supports:
:namefor named parameters*for one path segment**for the rest of a path
Wildcard matches are pushed into request data as positional args rather than named params.
For example, GET /files/report.pdf can produce:
req.data()
// { '0': 'report.pdf' }While GET /assets/images/icons/logo.png can produce:
req.data()
// { '0': 'images/icons/logo.png' }Flexible matching and router composition help large apps evolve route structure without forcing everything into one flat route table.
const admin = router();
admin.get('/admin/users/:id', ({ req, res }) => {
res.setJSON({ id: req.data('id') });
});
app.use(admin);- choose inline routes for directness
- choose decorated controllers for class-based grouping with explicit mounting
- choose entry routes for file-driven structure
- choose import routes for lazy loading and tooling
- choose view routes for template-first pages