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 and can be accessed via c.req.param()
:
site.get("/posts/:slug/comments/:commentId", (c) => {
const slug = c.req.param("slug");
const commentId = c.req.param("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 });
});
Response Types
Sapling provides several convenient methods for returning different types of responses:
// HTML Response
site.get("/page", (c) => {
return c.html("<h1>Welcome</h1>");
});
// JSON Response
site.get("/api/data", (c) => {
return c.json({ hello: "world" });
});
// Text Response
site.get("/text", (c) => {
return c.text("Plain text response");
});
// Redirect Response
site.get("/old-path", (c) => {
return c.redirect("/new-path");
});
// Custom Response
site.get("/custom", (c) => {
return new Response("Custom response", {
status: 201,
headers: {
"Content-Type": "text/plain",
"X-Custom-Header": "value"
}
});
});
404 Handling
Sapling provides a built-in 404 handler that returns "Not found" with a 404 status code. You can customize this behavior using setNotFoundHandler
:
site.setNotFoundHandler((c) => {
return c.html(`
<h1>Page Not Found</h1>
<p>The requested page could not be found.</p>
`);
});
The not found handler receives the same context object as regular routes and can return any valid response type.
Request Context
Each route handler receives a context object with the following properties and methods:
interface Context {
req: {
param: (name: string) => string;
method: string;
url: string;
headers: Headers;
};
state: Record<string, unknown>;
query: () => URLSearchParams;
jsonData: <T>() => Promise<T>;
formData: () => Promise<FormData>;
html: (content: string) => Response;
json: (data: unknown) => Response;
text: (content: string) => Response;
redirect: (location: string, status?: number) => Response;
}
Best Practices
Route Organization: Define specific routes before more general ones to ensure proper matching.
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.jsonData<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 new Response(JSON.stringify({ error: "Server Error" }), {
status: 500,
headers: { "Content-Type": "application/json" }
});
}
});
- Request Validation: Validate input data before processing:
site.post("/api/posts", async (c) => {
const data = await c.jsonData<{ title: string; content: string }>();
if (!data.title || !data.content) {
return new Response(JSON.stringify({ error: "Missing required fields" }), {
status: 400,
headers: { "Content-Type": "application/json" }
});
}
// Process valid data...
});