• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
lightweight

lightweight

scaffold

Unlisted
Like
scaffold
Home
Code
10
backend
4
frontend
4
shared
3
.vtignore
AGENTS.md
CLAUDE.md
README.md
deno.json
guidelines.md
main.http.tsx
Branches
1
Pull requests
Remixes
History
Environment variables
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
CLAUDE.md
Code
/
CLAUDE.md
Search
10/27/2025
Viewing readonly version of main branch: v55
View latest version
CLAUDE.md

CLAUDE.md

This file provides project-specific guidance to Claude Code when working with this Val Town application.

Note: For general Val Town platform guidelines, see AGENTS.md. This file contains architecture rules specific to THIS val.

Project Identity

This is a Notion webhook integration application that:

  • Receives webhooks from Notion when pages/databases change
  • Processes Notion data and integrates with external services (Firecrawler, etc.)
  • Saves processed results back to Notion
  • Provides a React frontend for viewing and managing Notion data

Scaffolding a New Val

If you're starting from scratch, follow these steps to scaffold the complete val structure:

1. Create Directory Structure

mkdir -p backend/controllers backend/routes/api backend/routes/tasks backend/routes/views backend/services mkdir -p frontend/components mkdir -p shared

2. Create main.http.tsx (Hono Entry Point)

Create main.http.tsx with:

  • Hono app initialization
  • Error unwrapper: app.onError((err, c) => { throw err; })
  • Static file serving for frontend and shared directories
  • Root route serving frontend/index.html
  • Commented route mounting examples
  • Export: export default app.fetch

3. Create Backend Service Files

Organize services by external API. For Notion, create a directory structure:

backend/services/notion/index.ts:

  • Import Notion client: import { Client } from "npm:@notionhq/client@2"
  • Initialize and export client: export const notion = new Client({ auth: Deno.env.get("NOTION_API_KEY") })
  • Re-export all functions from submodules:
    • export * from "./pages.ts"
    • export * from "./databases.ts"

backend/services/notion/pages.ts:

  • Import shared client: import { notion } from "./index.ts"
  • Export page-related functions:
    • getNotionPage(pageId) - Fetch page by ID
    • updateNotionPage(pageId, properties) - Update page properties
    • createNotionPage(databaseId, properties) - Create new page
    • getPageBlocks(pageId) - Get page content blocks

backend/services/notion/databases.ts:

  • Import shared client: import { notion } from "./index.ts"
  • Export database-related functions:
    • queryNotionDatabase(databaseId, filter?, sorts?) - Query database
    • getNotionDatabase(databaseId) - Get database info

Service function rules:

  • All functions return {success, data, error} format
  • Catch errors and return error objects (don't throw)
  • Use the shared notion client from index.ts

4. Create Backend Controller Files

backend/controllers/pageController.ts:

  • Import Notion service: import * as notionService from '../services/notion/index.ts'
  • Export controller functions:
    • getPage(pageId) - Get page with validation
    • updatePage(pageId, properties) - Update with validation
    • createPage(databaseId, properties) - Create with validation
    • getPageContent(pageId) - Get blocks with validation
  • All functions return {success, data, error, details?} format
  • Include filterSensitiveProperties() helper to remove button properties
  • Validate inputs before calling services

5. Create Authentication Middleware

backend/routes/authCheck.ts:

  • Import Hono types: import { Context, Next } from "npm:hono@4"
  • Export authCheck(c, next) function:
    • Check for API key in x-api-key header
    • Compare with Deno.env.get('API_KEY')
    • Return 401 if invalid
    • Call await next() if valid
  • Export verifyNotionWebhook(c, next) for webhook signature verification

6. Create Shared Types

shared/types.ts:

  • Define ControllerResponse<T> interface with {success, data, error, details?}
  • Define Notion types: NotionPage, NotionDatabase, NotionWebhookPayload
  • Define API types: UpdatePageRequest, CreatePageRequest, etc.
  • Use generic types that work in browser and Deno

7. Create Shared Utils

shared/utils.ts:

  • Export utility functions:
    • isValidPageId(pageId) - Validate Notion ID format
    • normalizePageId(pageId) - Remove hyphens
    • formatPageId(pageId) - Add hyphens in 8-4-4-4-12 format
    • getPlainText(richText) - Extract text from Notion rich text
    • getPropertyValue(properties, name) - Get value by property type
  • ONLY use standard JavaScript (no Deno or browser-specific APIs)

8. Create Frontend Files

frontend/index.html:

  • Include Pico CSS: <link href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.1.1/css/pico.classless.min.css" rel="stylesheet" />
  • Include error catching: <script src="https://esm.town/v/std/catch"></script>
  • Root div: <div id="root"></div>
  • Load React: <script type="module" src="/frontend/index.tsx"></script>

frontend/index.tsx:

  • JSX import source: /** @jsxImportSource https://esm.sh/react@18.2.0 */
  • Import React and ReactDOM with pinned versions
  • Import App component
  • Render <App /> to root element

frontend/components/App.tsx:

  • JSX import source and pinned React imports
  • Basic UI with example API call
  • State management for data fetching
  • Error and loading states

frontend/components/NotionBlock.tsx:

  • Render different Notion block types (paragraph, heading, lists, code, etc.)
  • Use semantic HTML (Pico CSS provides automatic styling)

frontend/components/NotionProperty.tsx:

  • Import getPropertyValue from shared/utils
  • Render different property types (title, select, date, checkbox, etc.)
  • Use semantic HTML (Pico CSS provides automatic styling)

9. Create README Files

Create README.md in:

  • Project root - Overview and architecture
  • backend/ - Backend structure and guidelines
  • frontend/ - Frontend structure and React guidelines
  • shared/ - Shared code guidelines

10. Environment Variables

Document required environment variables:

  • NOTION_API_KEY - Notion integration token (required)
  • API_KEY - API authentication (optional)
  • NOTION_WEBHOOK_SECRET - Webhook verification (optional)
  • Add service-specific keys as needed (e.g., FIRECRAWLER_API_KEY)

11. Verify Scaffold

After scaffolding, verify:

  • ✅ All directories exist
  • ✅ main.tsx exports app.fetch
  • ✅ Service functions return {success, data, error}
  • ✅ Controller functions return {success, data, error, details?}
  • ✅ Frontend has JSX import source and pinned React versions
  • ✅ Shared code has no Deno/browser-specific APIs
  • ✅ README files explain each directory's purpose

Import Patterns

This val runs in different environments, so import patterns matter.

Backend Imports (Deno only)

For code that ONLY runs in the backend (controllers, services, routes, main.tsx):

Use npm: specifier:

import { Hono } from "npm:hono@4"; import { Client } from "npm:@notionhq/client@2";

Benefits:

  • ✅ Modern Deno way
  • ✅ Faster resolution (no CDN roundtrip)
  • ✅ Better caching
  • ✅ More explicit about npm packages

Val Town utilities still use URLs:

import { readFile, serveFile } from "https://esm.town/v/std/utils@85-main/index.ts";

Frontend/Shared Imports (Browser compatible)

For code in frontend/ and shared/ that runs in the BROWSER:

Use esm.sh with version pins:

// Frontend React components import { useState } from "https://esm.sh/react@18.2.0?deps=react@18.2.0"; // Shared utilities (work in both browser and Deno) import { getPropertyValue } from "../../shared/utils.ts";

Why esm.sh for frontend:

  • ✅ Works in browser (no npm: support)
  • ✅ CDN delivery for client-side
  • ✅ Automatic bundling and transpilation

Quick Reference

LocationEnvironmentImport PatternExample
backend/Deno onlynpm:package@versionnpm:hono@4
main.tsxDeno onlynpm:package@versionnpm:hono@4
frontend/Browserhttps://esm.sh/package@versionhttps://esm.sh/react@18.2.0
shared/Bothhttps://esm.sh/package@versionhttps://esm.sh/date-fns@3
Val Town utilsDenohttps://esm.town/v/...Val Town standard library

Common Packages

Backend (use npm:):

  • Hono: npm:hono@4
  • Notion: npm:@notionhq/client@2
  • Database drivers, server-only utilities

Frontend (use esm.sh):

  • React: https://esm.sh/react@18.2.0
  • React DOM: https://esm.sh/react-dom@18.2.0/client
  • Browser-compatible libraries

Shared (use esm.sh):

  • Date utilities: https://esm.sh/date-fns@3
  • Validation libraries: https://esm.sh/zod@3
  • Any package used in both frontend and backend

Strict MVC Architecture

This val follows a 3-layer architecture. Every feature MUST follow this pattern. NEVER skip layers.

Request → Route → Controller → Service → External API
                                              ↓
Response ← Format ← Standard Response ← Result

Layer 1: Routes (backend/routes/)

Location:

  • routes/api/ - API endpoints for data operations
  • routes/tasks/ - Notion webhook handlers
  • routes/views/ - User-facing HTML/React views

Responsibility: HTTP ONLY

  • Extract request parameters (query, body, headers)
  • Apply authentication middleware from authCheck.ts
  • Call controller functions
  • Format controller responses into HTTP responses
  • Map success/error to HTTP status codes

Rules:

  • ❌ NO business logic in routes
  • ❌ NEVER call services directly from routes
  • ✅ Routes are thin wrappers around controllers
  • ✅ Always return proper HTTP responses with status codes:
    • 200 - Success
    • 400 - Validation errors, bad request
    • 401 - Authentication failures
    • 500 - Server/external service errors

Example pattern:

app.post('/api/tasks/:id', async (c) => { const pageId = c.req.param('id'); const body = await c.req.json(); const result = await pageController.updatePage(pageId, body); if (!result.success) { return c.json({ error: result.error, details: result.details }, 400); } return c.json(result.data); });

Layer 2: Controllers (backend/controllers/)

Responsibility: Business logic and orchestration

  • Validate request data
  • Orchestrate multiple service calls
  • Transform and filter data
  • Remove sensitive information

IMPORTANT: Work with Notion pages generically

  • The fundamental object in Notion is a page (not task, project, etc.)
  • Controllers and services should use pageId and work with any Notion page
  • Routes can be domain-specific (e.g., /api/tasks/:id), but they should call generic page controllers
  • This allows the architecture to handle any type of Notion page (tasks, projects, notes, etc.)

MUST return this exact structure:

{ success: boolean, data: any | null, error: string | null, details?: string // Additional error context }

Rules:

  • ❌ NEVER return HTTP responses (controllers are HTTP-agnostic)
  • ❌ NEVER call external APIs directly
  • ✅ Always call service layer functions
  • ✅ Return standardized response structure
  • ✅ Validate all input data
  • ✅ Filter sensitive data (e.g., button properties)

Example pattern:

export async function updatePage(pageId: string, properties: any) { // Validate if (!pageId || !properties) { return { success: false, data: null, error: "Invalid input", details: "pageId and properties are required" }; } // Call service const result = await notionService.updatePage(pageId, properties); if (!result.success) { return { success: false, data: null, error: "Failed to update page", details: result.error }; } return { success: true, data: result.data, error: null }; }

Layer 3: Services (backend/services/)

Responsibility: External API calls ONLY

  • Make HTTP requests to external APIs
  • Handle API authentication
  • Parse and normalize API responses
  • Manage retries and API-specific error handling

Create services for:

  • Notion API (notionService.ts)
  • Firecrawler (firecrawlerService.ts)
  • Val Town blob storage (blobService.ts)
  • Any other external service

Rules:

  • ✅ Return structured results with {success, data, error}
  • ✅ Handle all API errors gracefully (don't throw)
  • ✅ Use environment variables for API keys
  • ✅ Export typed functions

Example pattern:

export async function getNotionPage(pageId: string) { try { const notion = new Client({ auth: Deno.env.get('NOTION_API_KEY') }); const page = await notion.pages.retrieve({ page_id: pageId }); return { success: true, data: page, error: null }; } catch (err) { return { success: false, data: null, error: err.message }; } }

Adding New Endpoints - Step-by-Step

When asked to "build a /tasks/ endpoint" or any new feature, follow this checklist:

  1. ✅ Create route file in appropriate directory:

    • Webhook handlers → routes/tasks/
    • API endpoints → routes/api/
    • Views → routes/views/
  2. ✅ Extract request data in route (params, query, body)

  3. ✅ Apply authentication if needed using authCheck.ts middleware

  4. ✅ Create controller function in backend/controllers/[feature].ts

    • Implement validation
    • Implement business logic
    • Orchestrate service calls
  5. ✅ Create/update service functions in backend/services/[service].ts

    • Make external API calls
    • Return {success, data, error} structure
  6. ✅ Return standardized response from controller

  7. ✅ Format HTTP response in route based on controller result

  8. ✅ Add TypeScript types to shared/types.ts if data is shared across layers

  9. ✅ Import route into main.tsx and mount on Hono app

  10. ✅ Test the endpoint with curl or similar tool

Notion Integration Patterns

Webhook Handlers (routes/tasks/)

All Notion webhooks live in routes/tasks/. When creating webhook handlers:

  • Validate webhook signature/authentication
  • Extract Notion event data
  • Call appropriate controller
  • Return 200 status quickly (webhooks timeout)
  • Use async processing for slow operations

Notion Service Functions

Create dedicated service functions for common Notion operations:

// backend/services/notionService.ts export async function getNotionPage(pageId: string) { /* ... */ } export async function updateNotionPage(pageId: string, properties: any) { /* ... */ } export async function queryNotionDatabase(databaseId: string, filter?: any) { /* ... */ } export async function createNotionPage(databaseId: string, properties: any) { /* ... */ }

Always:

  • Use official Notion SDK: https://esm.sh/@notionhq/client
  • Use environment variable: Deno.env.get('NOTION_API_KEY')
  • Return standardized {success, data, error} structure

External Service Integration Pattern

For Simple Services (Single File)

When integrating a simple external service (like Firecrawler):

  1. Create service file: backend/services/[serviceName].ts

  2. Initialize client and export typed functions:

import { SomeClient } from "npm:some-client@1"; export const client = new SomeClient({ apiKey: Deno.env.get('SERVICE_API_KEY') }); export async function fetchFromService(params: ServiceParams) { try { const result = await client.fetch(params); return { success: true, data: result, error: null }; } catch (err: any) { return { success: false, data: null, error: err.message }; } }

For Complex Services (Directory Structure)

When integrating a complex service with multiple concerns (like Notion with pages, databases, blocks):

  1. Create service directory: backend/services/[serviceName]/

  2. Create index.ts - Initialize client and re-export:

import { Client } from "npm:@servicename/client@2"; export const client = new Client({ auth: Deno.env.get("SERVICE_API_KEY") }); export * from "./pages.ts"; export * from "./databases.ts";
  1. Create feature files - Import shared client:
// backend/services/notion/pages.ts import { client } from "./index.ts"; export async function getPage(id: string) { try { const result = await client.pages.get(id); return { success: true, data: result, error: null }; } catch (err: any) { return { success: false, data: null, error: err.message }; } }

Service Rules

  • Store API keys in environment: Deno.env.get('SERVICE_API_KEY')
  • Handle errors gracefully: Return error objects, don't throw
  • Add types to shared/types.ts if data is used across layers
  • Call service from controller, never from routes
  • One client instance: Initialize once, reuse everywhere

File Organization

Naming Conventions

  • Files: camelCase (e.g., notionService.ts, pageController.ts)
  • Functions: camelCase, verb-first (e.g., getNotionPage, updatePage)
  • Types/Interfaces: PascalCase (e.g., NotionWebhookPayload, PageResponse)
  • Routes: kebab-case URLs (e.g., /api/notion-tasks, /tasks/webhook-handler)

File Placement

  • Controllers: Use generic page controllers

    • pageController.ts - Generic Notion page operations (get, update, create)
    • webhookController.ts - Webhook processing logic
    • Note: Controllers should work with pageId generically, not task/project-specific
  • Services: Organize by external API

    • For complex services, use directories:
      • notion/ - Notion API integration
        • index.ts - Client initialization and re-exports
        • pages.ts - Page operations
        • databases.ts - Database operations
    • For simple services, use single files:
      • firecrawlerService.ts - Firecrawler integration
      • blobService.ts - Val Town blob storage
  • Routes: Organize by purpose

    • routes/api/ - RESTful API endpoints (/api/tasks, /api/tasks/:id)
    • routes/tasks/ - Webhook handlers (/tasks/notion-webhook)
    • routes/views/ - User-facing pages (/views/dashboard)
  • Shared: Cross-layer code

    • shared/types.ts - TypeScript interfaces
    • shared/utils.ts - Pure utility functions (work in browser AND Deno)

Error Handling Strategy

Services: Return errors, don't throw

return { success: false, data: null, error: "API call failed" };

Controllers: Add context to service errors

if (!result.success) { return { success: false, data: null, error: "Failed to update page", details: result.error }; }

Routes: Map to HTTP status codes

if (!result.success) { return c.json({ error: result.error, details: result.details }, 400); }

Add to main.tsx: Hono error unwrapper for better stack traces

app.onError((err, c) => { throw err; });

Authentication Rules

  • Auth middleware lives in routes/authCheck.ts
  • Apply to protected routes using Hono middleware pattern
  • Extract and validate API keys, tokens, or webhook signatures
  • Return 401 for authentication failures

Example:

import { authCheck } from './authCheck.ts'; app.post('/api/protected', authCheck, async (c) => { // Protected route logic });

Frontend Integration

Structure

  • frontend/index.html - HTML shell
  • frontend/index.tsx - React entry point
  • frontend/components/App.tsx - Main component
  • frontend/components/NotionBlock.tsx - Notion block renderer
  • frontend/components/NotionProperty.tsx - Property display

React Rules

ALWAYS:

  • Use React 18.2.0 with pinned dependencies
  • Include at top of file: /** @jsxImportSource https://esm.sh/react@18.2.0 */
  • Pin all React deps: ?deps=react@18.2.0,react-dom@18.2.0

Data Fetching:

  • Fetch from /api/* routes
  • Use shared types from shared/types.ts
  • Handle loading and error states

Frontend Styling

This val uses Pico CSS - a classless CSS framework that automatically styles semantic HTML.

Why Pico CSS?

  • No CSS classes needed - Keeps HTML clean and maintainable
  • Semantic HTML only - Use proper HTML elements (<article>, <main>, <kbd>, etc.)
  • Automatic styling - Beautiful design out of the box
  • CDN delivery - Single <link> tag, no build step

Rules

❌ NEVER use CSS class names (no className attributes) ❌ NEVER use TailwindCSS, Bootstrap, or other class-based frameworks ✅ ALWAYS use semantic HTML elements ✅ Use inline styles sparingly - Only for dynamic/conditional styling

Semantic HTML Elements

Use these semantic elements - Pico styles them automatically:

Layout:

  • <main> - Main content container
  • <article> - Self-contained content (cards, sections)
  • <section> - Thematic grouping of content
  • <header>, <footer> - Page/section headers and footers
  • <aside> - Sidebar content

Typography:

  • <h1> through <h6> - Headings
  • <p> - Paragraphs
  • <strong> - Bold/important text
  • <em> - Italic/emphasized text
  • <small> - Fine print
  • <mark> - Highlighted text
  • <code> - Inline code
  • <pre><code> - Code blocks
  • <kbd> - Keyboard input / tags

Lists:

  • <ul>, <ol>, <li> - Lists
  • <dl>, <dt>, <dd> - Definition lists

Forms:

  • <input>, <textarea>, <select> - Form inputs
  • <button> - Buttons
  • <label> - Form labels
  • Use aria-busy attribute for loading states on buttons

Links & Media:

  • <a> - Links
  • <time> - Dates and times
  • <figure>, <figcaption> - Images with captions

Example Patterns

Card layout:

Create val
<article> <h2>Card Title</h2> <p>Card content goes here.</p> <button>Action</button> </article>

Form with loading state:

Create val
<div style={{ display: 'flex', gap: '1rem' }}> <input type="text" placeholder="Enter value" style={{ flex: 1 }} /> <button disabled={loading} aria-busy={loading}> Submit </button> </div>

Error message:

Create val
{error && <mark>{error}</mark>}

Tags/badges:

Create val
<kbd>Tag 1</kbd> <kbd>Tag 2</kbd>

Conditional styling:

Create val
<span style={checked ? { textDecoration: 'line-through', opacity: 0.6 } : undefined}> Task text </span>

Loading Pico CSS

Already included in frontend/index.html:

<link href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.1.1/css/pico.classless.min.css" rel="stylesheet" />

Shared Code (shared/)

Types (shared/types.ts):

  • All TypeScript interfaces shared between frontend/backend
  • Notion data structures
  • API request/response types
  • Common data models

Utils (shared/utils.ts):

  • Pure functions that work in both Deno (backend) and browser (frontend)
  • ❌ NEVER use Deno APIs (won't work in browser)
  • ✅ Only use standard JavaScript/TypeScript

Environment Variables

Required environment variables for this val:

  • NOTION_API_KEY - Notion integration token
  • FIRECRAWLER_API_KEY - Firecrawler service (if used)
  • Additional service API keys as needed

Access via: Deno.env.get('VAR_NAME')

NEVER hardcode secrets - always use environment variables

Data Validation

Validate request data in controllers, not routes:

if (!pageId || typeof pageId !== 'string') { return { success: false, data: null, error: "Invalid pageId", details: "pageId must be a non-empty string" }; }

Check:

  • Required fields exist
  • Types are correct
  • Formats are valid
  • Values are within expected ranges

Return helpful error messages with details field

Response Filtering

Filter sensitive data in controllers before returning:

  • Remove button properties from Notion data
  • Don't expose internal IDs or tokens
  • Clean up API responses before sending to frontend
  • Strip unnecessary metadata

Example:

// Remove sensitive properties const filtered = { ...notionData, properties: Object.entries(notionData.properties) .filter(([key, value]) => value.type !== 'button') .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) };

Questions to Ask User

When the user requests a new feature, clarify:

  1. Is this a webhook handler or API endpoint?

    • Webhook → routes/tasks/
    • API → routes/api/
  2. What external service needs integration?

    • New service → create backend/services/[name].ts
  3. What data needs to be saved back to Notion?

    • Update Notion → use/extend notionService.ts
  4. Does this need authentication?

    • Protected → apply authCheck middleware
  5. Is this frontend-facing?

    • Yes → create React component in frontend/components/

Common Patterns Checklist

Before implementing, verify:

  • ✅ Following 3-layer architecture (Route → Controller → Service)
  • ✅ Routes only handle HTTP, no business logic
  • ✅ Controllers return standardized {success, data, error} structure
  • ✅ Services handle external API calls
  • ✅ Never skipping layers
  • ✅ TypeScript types defined in shared/types.ts
  • ✅ Environment variables used for secrets
  • ✅ Error handling at each layer
  • ✅ Validation in controllers
  • ✅ Sensitive data filtered before returning

Integration with AGENTS.md

Use AGENTS.md for:

  • Val Town platform requirements
  • Deno-specific patterns
  • Standard library usage
  • React configuration
  • Import patterns (esm.sh)
  • Platform limitations

Use CLAUDE.md (this file) for:

  • This val's architecture
  • MVC layer separation
  • Notion integration patterns
  • External service integration
  • File organization
  • Response formats

Entry Point

main.tsx is the HTTP entry point:

  • Exports app.fetch for Val Town
  • Uses Hono for routing
  • Serves static frontend assets
  • Mounts API routes at /api
  • Includes error handler

Always mount new routes in main.tsx:

import { taskRoutes } from './backend/routes/tasks/index.ts'; app.route('/tasks', taskRoutes);

Hono Framework Patterns

This val uses Hono as the web framework. Follow these patterns:

Importing Hono

import { Hono } from "npm:hono@4"; import { Context, Next } from "npm:hono@4";

Note: Use npm: specifier for backend-only packages (better performance, modern Deno way).

Creating Route Modules

When creating route modules (e.g., backend/routes/api/pages.ts):

import { Hono } from "npm:hono@4"; import * as pageController from '../../controllers/pageController.ts'; const pages = new Hono(); // GET /api/pages/:id pages.get('/:id', async (c) => { const pageId = c.req.param('id'); const result = await pageController.getPage(pageId); if (!result.success) { return c.json({ error: result.error, details: result.details }, 400); } return c.json(result.data); }); // POST /api/pages/:id pages.post('/:id', async (c) => { const pageId = c.req.param('id'); const body = await c.req.json(); const result = await pageController.updatePage(pageId, body.properties); if (!result.success) { return c.json({ error: result.error, details: result.details }, 400); } return c.json(result.data); }); export default pages;

Mounting Routes

Create an index file for each route directory (e.g., backend/routes/api/index.ts):

import { Hono } from "npm:hono@4"; import pages from './pages.ts'; const api = new Hono(); api.route('/pages', pages); // Add more routes as needed export default api;

Then mount in main.tsx:

import api from './backend/routes/api/index.ts'; app.route('/api', api);

Accessing Request Data

// URL params const id = c.req.param('id'); // Query params const query = c.req.query('q'); // Headers const apiKey = c.req.header('x-api-key'); // JSON body const body = await c.req.json(); // Form data const formData = await c.req.formData();

Returning Responses

// JSON response return c.json({ data: 'value' }); // JSON with status code return c.json({ error: 'Not found' }, 404); // HTML response return c.html('<h1>Hello</h1>'); // Plain text return c.text('Hello'); // Redirect (use this pattern, not Response.redirect) return new Response(null, { status: 302, headers: { Location: '/new-url' }});

Middleware

Apply middleware to routes:

import { authCheck } from '../authCheck.ts'; // Apply to single route pages.post('/:id', authCheck, async (c) => { // Protected route logic }); // Apply to all routes in this module pages.use('*', authCheck);

Error Handler

Already configured in main.tsx:

app.onError((err, c) => { throw err; // Re-throw to see full stack trace });

Static File Serving

Already configured in main.tsx:

import { serveFile } from "https://esm.town/v/std/utils@85-main/index.ts"; app.get("/frontend/*", c => serveFile(c.req.path, import.meta.url)); app.get("/shared/*", c => serveFile(c.req.path, import.meta.url));

Route Organization Example

backend/routes/
├── api/
│   ├── index.ts          # Mounts all API routes
│   ├── pages.ts          # /api/pages/* endpoints
│   └── databases.ts      # /api/databases/* endpoints
├── tasks/
│   ├── index.ts          # Mounts all webhook routes
│   └── notion.ts         # /tasks/notion webhook handler
├── views/
│   └── dashboard.ts      # /views/dashboard page
└── authCheck.ts          # Auth middleware
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2025 Val Town, Inc.