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:
ControllerResponse<T>)NotionPage, NotionDatabase, etc.)UpdatePageRequest, PageResponse, etc.)Naming Conventions:
NotionWebhookPayload, PageResponse)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:
What Does NOT Go Here:
Deno.env, Deno.readFile, etc.) → use backend/utils/window, document, localStorage) → use frontend codefs, path, etc.)backend/services/backend/controllers/Rules:
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:
Examples:
formatPageId(id)getPlainText(richText)isValidEmail(email)chunk(array, size)Use when the function:
Examples:
sha256(message) using Web Crypto APIsignToken(payload) using Deno cryptoparseEnvConfig()hashFile(path)types.ts?/**
* Response from page update operations
*/
export interface PageUpdateResponse {
page: NotionPage;
updatedAt: string;
changes: string[];
}
/**
* 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, '-');
}
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 and backend/utils//frontend/README.md - Frontend architectureCLAUDE.md - Complete project architecture guidelines