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

  1. Route Organization: Define specific routes before more general ones to ensure proper matching.

  2. 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);
});
  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 new Response(JSON.stringify({ error: "Server Error" }), {
      status: 500,
      headers: { "Content-Type": "application/json" }
    });
  }
});
  1. 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...
});