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
- Versioning: Consider versioning your API routes:
site.get("/api/v1/users", (c) => {
// V1 implementation
});
site.get("/api/v2/users", (c) => {
// V2 implementation
});
- 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 });
});
- 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.