Routing
Sapling uses a simple and powerful routing system based on URL pattern matching. Routes are defined explicitly using the Sapling instance methods, providing a clean and type-safe way to handle HTTP requests.
Route Handlers
Routes are defined using the Sapling instance methods for different HTTP methods:
import { Sapling } from '@sapling/sapling';
const site = new Sapling();
// Basic route
site.get("/", (c) => {
return c.html("<h1>Welcome to Sapling</h1>");
});
// Dynamic route with parameters
site.get("/users/:id", (c) => {
const userId = c.req.param("id");
return c.json({ userId });
});
Each route handler receives a context object (c
) and can return one of the following types:
Response
Promise<Response>
Promise<Response | null>
null
URL Pattern Matching
Sapling uses the URLPattern API for route matching. Each route is automatically registered with and without a trailing slash, so both /users
and /users/
will match the same handler.
Route Parameters
Dynamic segments in routes are defined using the :parameter
syntax. Parameters can be accessed in two ways:
site.get("/posts/:slug/comments/:commentId", (c) => {
// Get a single parameter
const slug = c.req.param("slug");
const commentId = c.req.param("commentId");
// Get all parameters as an object
const params = c.req.param();
// params = { slug: "...", commentId: "..." }
return c.json({ slug, commentId });
});
HTTP Methods
Sapling supports all standard HTTP methods:
// GET request
site.get("/api/data", (c) => {
return c.json({ data: "here" });
});
// POST request
site.post("/api/users", async (c) => {
const data = await c.req.json();
return c.json({ created: true });
});
// PUT request
site.put("/api/users/:id", async (c) => {
const id = c.req.param("id");
const data = await c.req.json();
return c.json({ updated: id });
});
// DELETE request
site.delete("/api/users/:id", (c) => {
const id = c.req.param("id");
return c.json({ deleted: id });
});
// PATCH request
site.patch("/api/users/:id", async (c) => {
const id = c.req.param("id");
const data = await c.req.json();
return c.json({ patched: id });
});
Request Context
The context object provides various methods and properties for handling requests:
interface Context {
req: {
// Get URL parameters
param(): Record<string, string>;
param(name: string): string;
// Request properties
method: string;
url: string;
headers: Headers;
// Helper methods
header(name: string): string | undefined;
json<T = unknown>(): Promise<T>;
formData(): Promise<FormData>;
text(): Promise<string>;
};
// Response headers
res: {
headers: Headers;
};
// State management
state: Record<string, unknown>;
set<T>(key: string, value: T): void;
get<T>(key: string): T | undefined;
// Helper methods
query(): URLSearchParams;
html(content: string | ReadableStream): Response;
json(data: unknown, status?: number): Response;
text(content: string, status?: number): Response;
redirect(location: string, status?: number): Response;
}
Middleware
Sapling supports both global and route-specific middleware:
// Global middleware
site.use(async (c, next) => {
console.log("Request started");
const response = await next();
console.log("Request ended");
return response;
});
// Route-specific middleware
site.get("/protected",
async (c, next) => {
const token = c.req.header("Authorization");
if (!token) {
return c.json({ error: "Unauthorized" }, 401);
}
return await next();
},
(c) => {
return c.json({ message: "Protected data" });
}
);
Prerendering
Sapling supports prerendering routes for static site generation. In development mode, prerendered routes behave like normal .get
routes, while in production they are served as static HTML files.
// Static route
site.prerender("/about", (c) => {
return c.html("<h1>About Us</h1>");
});
// Dynamic route with parameters
site.prerender("/blog/:slug", async (c) => {
const post = await getPost(c.req.param("slug"));
return c.html(`<h1>${post.title}</h1>${post.content}`);
}, [
{ slug: "first-post" },
{ slug: "second-post" }
]);
For detailed information about prerendering, including build setup and best practices, see the Prerendering guide.
404 Handling
You can customize the 404 handler using setNotFoundHandler
:
site.notFound((c) => {
return c.html(`
<h1>Page Not Found</h1>
<p>The requested page could not be found.</p>
`);
});
Best Practices
- Type Safety: Use TypeScript interfaces for request and response data:
interface User {
id: string;
name: string;
}
site.post("/api/users", async (c) => {
const data = await c.req.json<User>();
return c.json(data);
});
- Error Handling: Implement proper error handling in your routes:
site.get("/api/protected", async (c) => {
try {
const data = await fetchData();
return c.json(data);
} catch (error) {
return c.json({ error: "Server Error" }, 500);
}
});
- Request Validation: Validate input data before processing:
site.post("/api/posts", async (c) => {
const data = await c.req.json<{ title: string; content: string }>();
if (!data.title || !data.content) {
return c.json({ error: "Missing required fields" }, 400);
}
// Process valid data...
});