Complete API reference for routing in Jen.js.
Types
LoaderContext
Data passed to loader functions:
interface LoaderContext {
params: Record<string, string | string[]>;
request: IncomingMessage;
response: ServerResponse;
query: URLSearchParams;
}
Route
Internal route representation:
interface Route {
path: string; // /posts/:id
file: string; // site/posts/($id).tsx
component: (props: any) => JSX.Element;
loader?: (ctx: LoaderContext) => Promise<any>;
head?: (props: any) => JSX.Element;
staticPaths?: () => Promise<StaticPath[]>;
mode?: 'ssr' | 'ssg';
revalidate?: number;
}
Loader Functions
Export async function to load data:
export async function loader(ctx: LoaderContext) {
return {
title: 'Page Title',
data: await fetchData()
};
}
export default function Page({ data }: any) {
return <h1>{data.title}</h1>;
}
Access Parameters
export async function loader(ctx: LoaderContext) {
const id = ctx.params.id; // Route parameter
const page = ctx.query.get('page'); // Query parameter
const token = ctx.request.headers.authorization;
return { /* ... */ };
}
Access Request/Response
export async function loader(ctx: LoaderContext) {
const method = ctx.request.method;
const url = ctx.request.url;
const headers = ctx.request.headers;
// Set response headers
ctx.response.setHeader('Cache-Control', 'max-age=3600');
return { /* ... */ };
}
Head Function
Custom head elements per page:
export function Head({ data }: any) {
return (
<>
<title>{data.title}</title>
<meta name="description" content={data.description} />
<meta property="og:title" content={data.title} />
<meta property="og:image" content={data.image} />
<link rel="canonical" href={`https://example.com${data.path}`} />
</>
);
}
Static Paths
Precompute paths for dynamic routes (SSG):
export async function staticPaths() {
const posts = await getAllPosts();
return posts.map(post => ({
slug: post.slug,
category: post.category
}));
}
export async function loader(ctx: LoaderContext) {
const post = await getPost(ctx.params.slug);
return { post };
}
Generates routes for all returned paths.
Route Matching
Static Routes
Exact path matching:
site/(about).tsx → /about (only)
Dynamic Routes
Parameter matching:
site/posts/($id).tsx → /posts/* (any id)
site/users/($userId)/posts/($postId).tsx → /users/*/posts/*
Parameters captured as ctx.params.id, ctx.params.userId, etc.
Catch-All Routes
Matches remaining segments:
site/(...rest).tsx → /* (any path)
site/docs/(...path).tsx → /docs/* (under /docs)
Access remaining segments:
export async function loader(ctx: LoaderContext) {
const segments = ctx.params.rest; // Array of segments
const path = segments.join('/');
return { path };
}
Query Parameters
Access URL query string:
export async function loader(ctx: LoaderContext) {
const page = ctx.query.get('page');
const search = ctx.query.get('search');
const filters = ctx.query.getAll('filter');
return { page, search, filters };
}
Route Configuration
Override defaults per route:
// Rendering mode
export const mode = 'ssr' | 'ssg';
// Revalidation time (seconds)
export const revalidate = 3600;
// Route metadata
export const meta = {
title: 'Page Title',
description: 'Page description'
};
// Middleware
export const middleware = [authMiddleware];
Error Handling
export async function loader(ctx: LoaderContext) {
try {
const data = await fetchData();
return { data };
} catch (err) {
// Return error page
ctx.response.writeHead(500);
ctx.response.end('Server error');
return {};
}
}
Redirects
export async function loader(ctx: LoaderContext) {
if (!isAuthenticated(ctx)) {
ctx.response.writeHead(302, {
location: '/login'
});
ctx.response.end();
return {};
}
return { /* ... */ };
}
Middleware in Routes
Apply middleware to specific routes:
import { authMiddleware } from '@src/middleware/auth';
export const middleware = [authMiddleware];
export async function loader(ctx: LoaderContext) {
// User is authenticated
const userId = ctx.request.user?.id;
return { userId };
}
Component Props
interface PageProps {
data?: any; // From loader
params?: Record<string, any>; // Route params
query?: URLSearchParams; // Query string
}
export default function Page({ data, params, query }: PageProps) {
return <h1>{data?.title}</h1>;
}
API Routes
Exports handle function instead of default:
import type { IncomingMessage, ServerResponse } from 'node:http';
export async function handle(
req: IncomingMessage,
res: ServerResponse
) {
if (req.method === 'GET') {
res.writeHead(200);
res.end('GET response');
}
}
Query Parameters in API Routes
export async function handle(req: IncomingMessage, res: ServerResponse) {
const url = new URL(req.url!, `http://${req.headers.host}`);
const page = url.searchParams.get('page');
const limit = url.searchParams.get('limit');
// Use page, limit
}
Route Order
Routes matched by specificity:
- Exact matches (
/aboutbefore($slug)) - Specific dynamic routes before catch-alls
- More nested routes before less nested
File Extensions
Supported route file extensions:
.tsx— Page component.jsx— Page component (not recommended).ts— API route.js— API route (not recommended)
Naming Conventions
Good route names:
(home).tsx— Homepage(blog).tsx— Blog index($slug).tsx— Dynamic route(...rest).tsx— Catch-all- (...rest) — Avoid dots, use hyphens
Bad route names:
home.tsx— Missing parentheses(home page).tsx— Space in name(.tsx— Syntax errorhome-page.tsx— Missing parentheses