Unlisted
Like
scaffold
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.
Viewing readonly version of main branch: v74View latest version
This directory contains code shared between frontend (browser) and backend (Deno).
Critical Rule: All code here MUST work in both browser and Deno environments.
Purpose: Define data structures shared across all layers of the application.
What Goes Here:
- ✅ Controller response format (
ControllerResponse<T>) - ✅ Notion data structures (
NotionPage,NotionDatabase, etc.) - ✅ API request/response types (
UpdatePageRequest,PageResponse, etc.) - ✅ Common data models used in both frontend and backend
- ✅ Enum types for shared constants
Naming Conventions:
- Interfaces/Types: PascalCase (e.g.,
NotionWebhookPayload,PageResponse) - Enum values: SCREAMING_SNAKE_CASE or PascalCase depending on usage
Example:
// Standardized controller response
export interface ControllerResponse<T> {
success: boolean;
data: T | null;
error: string | null;
details?: string;
}
// Notion page structure
export interface NotionPage {
id: string;
properties: Record<string, any>;
created_time: string;
last_edited_time: string;
}
// API request type
export interface UpdatePageRequest {
properties: Record<string, any>;
}
Purpose: Pure utility functions that work in BOTH browser and Deno environments.
Critical Constraint: NO platform-specific APIs allowed.
What Goes Here:
- ✅ String manipulation functions
- ✅ Date formatting (using standard Date API only)
- ✅ Data transformation functions
- ✅ Validation helpers (using standard JavaScript)
- ✅ Array/object utilities
- ✅ Notion-specific helpers (ID formatting, rich text extraction)
What Does NOT Go Here:
- ❌ Deno APIs (
Deno.env,Deno.readFile, etc.) → usebackend/utils/ - ❌ Browser APIs (
window,document,localStorage) → use frontend code - ❌ Node APIs (
fs,path, etc.) - ❌ External API calls → use
backend/services/ - ❌ Business logic → use
backend/controllers/
Rules:
- Pure functions with predictable outputs
- No side effects (no global state, no I/O)
- Single responsibility per function
- Type-safe with TypeScript
- Well-documented with JSDoc comments
Example Functions:
/**
* Check if a string is a valid Notion page ID format
*/
export function isValidPageId(pageId: string): boolean {
if (!pageId || typeof pageId !== 'string') {
return false;
}
const cleanId = pageId.replace(/-/g, '');
return cleanId.length === 32 && /^[a-f0-9]+$/i.test(cleanId);
}
/**
* Format a Notion page ID with hyphens in standard format (8-4-4-4-12)
*/
export function formatPageId(pageId: string): string {
const clean = pageId.replace(/-/g, '');
return `${clean.slice(0, 8)}-${clean.slice(8, 12)}-${clean.slice(12, 16)}-${clean.slice(16, 20)}-${clean.slice(20)}`;
}
/**
* Extract plain text from Notion rich text array
*/
export function getPlainText(richText: any[]): string {
if (!Array.isArray(richText)) {
return '';
}
return richText.map(text => text.plain_text || '').join('');
}
/**
* Get a property value from Notion page properties by type
*/
export function getPropertyValue(properties: any, propertyName: string): any {
const property = properties[propertyName];
if (!property) return null;
switch (property.type) {
case 'title':
return getPlainText(property.title);
case 'rich_text':
return getPlainText(property.rich_text);
case 'number':
return property.number;
case 'select':
return property.select?.name || null;
case 'date':
return property.date?.start || null;
// ... more types
default:
return property;
}
}
// frontend/components/App.tsx
import { getPropertyValue } from "../../shared/utils.ts";
import type { NotionPage } from "../../shared/types.ts";
// backend/controllers/pageController.ts
import { isValidPageId } from '../../shared/utils.ts';
import type { ControllerResponse } from '../../shared/types.ts';
Use when the function:
- Works in both browser AND Deno
- Uses only standard JavaScript/TypeScript
- Has no platform-specific dependencies
Examples:
- String formatting:
formatPageId(id) - Data extraction:
getPlainText(richText) - Simple validation:
isValidEmail(email) - Array utilities:
chunk(array, size)
Use when the function:
- Only runs in Deno (backend)
- Needs Deno-specific APIs (Web Crypto, file system, etc.)
- Is never called from frontend
Examples:
- Cryptography:
sha256(message)using Web Crypto API - JWT signing:
signToken(payload)using Deno crypto - Environment parsing:
parseEnvConfig() - File hashing:
hashFile(path)
- Define interface in
types.ts - Use clear, descriptive PascalCase names
- Document complex types with JSDoc comments
- Include all required fields, mark optional with
? - Use generics where appropriate
/**
* Response from page update operations
*/
export interface PageUpdateResponse {
page: NotionPage;
updatedAt: string;
changes: string[];
}
- Verify function works in BOTH browser and Deno
- Test with standard JavaScript only (no platform APIs)
- Write as pure function (no side effects)
- Add JSDoc documentation
- Use TypeScript types for parameters and return values
- Keep functions small and focused
/**
* Normalize a Notion page ID by removing hyphens
* @param pageId - The page ID with or without hyphens
* @returns The normalized ID (32 hex characters)
*/
export function normalizePageId(pageId: string): string {
return pageId.replace(/-/g, '');
}
To ensure code works in both environments:
Open browser console and test:
// Test your function
const result = getPlainText([{plain_text: "Hello"}]);
console.log(result); // Should work
// Test in Deno REPL or script
import { getPlainText } from "./shared/utils.ts";
console.log(getPlainText([{plain_text: "Hello"}])); // Should work
// ❌ BAD - Uses Deno API
export function getEnv(key: string): string {
return Deno.env.get(key) || ''; // Won't work in browser!
}
// ❌ BAD - Uses browser API
export function saveToLocal(key: string, value: string): void {
localStorage.setItem(key, value); // Won't work in Deno!
}
// ❌ BAD - Uses Node API
export function readFile(path: string): string {
return require('fs').readFileSync(path); // Won't work anywhere in Val Town!
}
// ✅ GOOD - Pure JavaScript
export function toKebabCase(str: string): string {
return str.toLowerCase().replace(/\s+/g, '-');
}
- Platform Independence: Never use Deno, Node, or browser-specific APIs
- Pure Functions: No side effects, predictable outputs
- Type Safety: Always use TypeScript types
- Documentation: Add JSDoc comments for public functions
- Single Responsibility: Each function does one thing well
- Test Both Environments: Verify code works in browser and Deno
- Standard Library Only: Use only JavaScript standard library features
- No External Calls: No API calls, database queries, or file I/O
isValidPageId(id) // Validate format
normalizePageId(id) // Remove hyphens
formatPageId(id) // Add hyphens (8-4-4-4-12)
getPlainText(richText) // Extract text from rich text
getPropertyValue(properties, name) // Get typed property value
isValidEmail(email) // Email format check
isValidUrl(url) // URL format check
isEmpty(value) // Check for empty values
slugify(text) // Convert to URL-safe slug
truncate(text, length) // Truncate with ellipsis
capitalize(text) // Capitalize first letter
/backend/README.md- Backend architecture andbackend/utils//frontend/README.md- Frontend architectureCLAUDE.md- Complete project architecture guidelines