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

  1. 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);
});
  1. 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);
  }
});
  1. 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...
});