API Routes

API routes allow you to create server-side endpoints that can handle HTTP requests and return JSON or other data formats. Sapling makes it easy to create API routes alongside your regular page routes.

Basic API Route

Create an API route by using the site.get(), site.post(), or other HTTP method handlers:

// routes/api/hello.ts
site.get("/api/hello", (c) => {
  return c.json({ message: "Hello from Sapling API!" });
});

HTTP Methods

Sapling supports all standard HTTP methods:

// GET request
site.get("/api/users", (c) => {
  return c.json({ users: ["Alice", "Bob"] });
});

// POST request
site.post("/api/users", async (c) => {
  const body = await c.req.json();
  // Handle user creation
  return c.json({ success: true });
});

// PUT request
site.put("/api/users/:id", async (c) => {
  const id = c.req.param("id");
  const body = await c.req.json();
  // Handle user update
  return c.json({ success: true });
});

// DELETE request
site.delete("/api/users/:id", (c) => {
  const id = c.req.param("id");
  // Handle user deletion
  return c.json({ success: true });
});

// PATCH request
site.patch("/api/users/:id", async (c) => {
  const id = c.req.param("id");
  const body = await c.req.json();
  // Handle partial user update
  return c.json({ success: true });
});

Request Handling

Query Parameters

Access query parameters using c.query():

site.get("/api/search", (c) => {
  const query = c.query().get("q");
  const page = parseInt(c.query().get("page") || "1");
  
  return c.json({
    query,
    page,
    results: []
  });
});

Route Parameters

Access route parameters using c.req.param():

site.get("/api/users/:id/posts/:postId", (c) => {
  const userId = c.req.param("id");
  const postId = c.req.param("postId");
  
  return c.json({
    userId,
    postId
  });
});

Request Body

Parse JSON request bodies using c.jsonData():

site.post("/api/posts", async (c) => {
  const body = await c.jsonData<{ title: string; content: string }>();
  
  // Validate the request body
  if (!body.title || !body.content) {
    return new Response(JSON.stringify({ error: "Missing required fields" }), {
      status: 400,
      headers: { "Content-Type": "application/json" }
    });
  }
  
  // Process the post
  return c.json({
    id: "new-post-id",
    ...body
  });
});

Form Data

Handle form data using c.formData():

site.post("/api/upload", async (c) => {
  const form = await c.req.formData();
  const file = form.get("file");
  
  // Process the file
  return c.json({ success: true });
});

Response Types

JSON Responses

Return JSON responses using c.json():

site.get("/api/data", (c) => {
  return c.json({
    success: true,
    data: {
      message: "Hello World"
    }
  });
});

HTML Responses

Return HTML responses using c.html():

site.get("/page", (c) => {
  return c.html("<h1>Welcome to my page</h1>");
});

Text Responses

Return plain text responses using c.text():

site.get("/plain", (c) => {
  return c.text("Hello World");
});

Custom Responses

For custom status codes or headers, use the Response object directly:

site.post("/api/items", async (c) => {
  // Created successfully
  return new Response(JSON.stringify({ id: "new-item" }), {
    status: 201,
    headers: { "Content-Type": "application/json" }
  });
});

site.get("/api/protected", (c) => {
  // Unauthorized
  return new Response(JSON.stringify({ error: "Unauthorized" }), {
    status: 401,
    headers: { "Content-Type": "application/json" }
  });
});

Redirects

Perform redirects using c.redirect():

site.get("/old-page", (c) => {
  return c.redirect("/new-page");
});

site.get("/permanent-redirect", (c) => {
  return c.redirect("/new-location", 301);
});

Error Handling

Implement error handling for your API routes:

site.get("/api/protected-resource", async (c) => {
  try {
    // Check authentication
    const user = await authenticateUser(c);
    if (!user) {
      return new Response(JSON.stringify({ error: "Unauthorized" }), {
        status: 401,
        headers: { "Content-Type": "application/json" }
      });
    }

    // Process request
    const data = await fetchProtectedData();
    return c.json(data);
  } catch (error) {
    console.error("API Error:", error);
    return new Response(JSON.stringify({ error: "Internal Server Error" }), {
      status: 500,
      headers: { "Content-Type": "application/json" }
    });
  }
});

TypeScript Support

Use TypeScript interfaces for type-safe API handling:

interface User {
  id: string;
  name: string;
  email: string;
}

interface CreateUserBody {
  name: string;
  email: string;
}

site.post("/api/users", async (c) => {
  const body = await c.jsonData<CreateUserBody>();
  
  const user: User = {
    id: crypto.randomUUID(),
    name: body.name,
    email: body.email
  };
  
  return c.json(user);
});

Best Practices

  1. Versioning: Consider versioning your API routes:
site.get("/api/v1/users", (c) => {
  // V1 implementation
});

site.get("/api/v2/users", (c) => {
  // V2 implementation
});
  1. Validation: Validate request data before processing:
site.post("/api/comments", async (c) => {
  const body = await c.jsonData<{ content: string }>();
  
  if (!body.content || body.content.length < 10) {
    return new Response(JSON.stringify({
      error: "Comment must be at least 10 characters"
    }), {
      status: 400,
      headers: { "Content-Type": "application/json" }
    });
  }
  
  // Process valid comment
  return c.json({ success: true });
});
  1. Custom 404 Handler: Set up a custom 404 handler:
site.setNotFoundHandler((c) => {
  return new Response("Custom Not Found", { 
    status: 404,
    headers: { "Content-Type": "text/plain" }
  });
});

Testing API Routes

Create tests for your API routes:

// tests/api.test.ts
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

Deno.test("GET /api/hello returns correct message", async () => {
  const response = await fetch("http://localhost:3000/api/hello");
  const data = await response.json();
  
  assertEquals(response.status, 200);
  assertEquals(data.message, "Hello from Sapling API!");
});

For more examples and use cases, check out our Examples section.