Here are the step-by-step instructions that would lead directly to the correct, clean implementation:
- Endpoint: GET /views/glimpse/:id
- Authentication: Required (existing Google OAuth)
- Input: Notion page ID (not database ID)
- Output: JSON response with full service response (minus UI elements)
- Data source: Notion page properties
cat /backend/services/notion.service.ts
cat /backend/routes/views/\_views.routes.ts
cat /main.tsx # Look for auth middleware setup
- Current service has getDatabaseById() for database metadata
- Need getPageById() for individual page data
- Service should return full Notion response for flexibility
// Add to /backend/services/notion.service.ts
export async function getPageById(pageId: string) {
try {
const response = await notion.pages.retrieve({
page_id: pageId,
});
return {
success: true,
data: response,
timestamp: new Date().toISOString(),
};
} catch (error) {
return {
success: false,
error: error.message,
timestamp: new Date().toISOString(),
};
}
}
// Add to /backend/services/notion.service.ts
export async function getDatabasePages(databaseId: string) {
try {
const response = await notion.databases.query({
database_id: databaseId,
});
return {
success: true,
data: response,
timestamp: new Date().toISOString(),
};
} catch (error) {
return {
success: false,
error: error.message,
timestamp: new Date().toISOString(),
};
}
}
// Create /backend/controllers/glimpse.controller.ts
import { Context } from "npm:hono@3.12.12";
import { getPageById } from "../services/notion.service.ts";
export async function glimpseHandler(c: Context) {
const id = c.req.param("id");
if (!id) {
return c.json({ error: "Page ID is required" }, 400);
}
const result = await getPageById(id);
if (!result.success) {
return c.json({ error: "Failed to fetch page data", details: result.error }, 500);
}
// Filter out button properties from the response
if (result.data?.properties) {
const filteredProperties = Object.fromEntries(
Object.entries(result.data.properties).filter(([key, value]) => value?.type !== "button")
);
result.data.properties = filteredProperties;
}
return c.json(result);
}
// Update /backend/routes/views/\_views.routes.ts
import { Hono } from "npm:hono@3.12.12";
import { glimpseHandler } from "../../controllers/glimpse.controller.ts";
const app = new Hono();
app.get("/glimpse/:id", glimpseHandler);
export default app;
Add to /backend/routes/views/README.md
- Purpose: Returns Notion page data as JSON (filtered for data consumption)
- Authentication: Required (Google OAuth)
- Parameters:
id
- Notion page ID - Response: Service response object with Notion page data (button properties removed)
- Filtering: Removes UI-specific properties (type: "button") for cleaner data consumption
✅ Controller Best Practices:
- Single Responsibility: Controller handles HTTP concerns + minimal data cleanup
- Thin Layer: Minimal processing, focused on improving data consumption
- Useful Filtering: Remove UI elements that don't belong in data APIs
- Consistent Error Handling: Standard error response format
✅ Service Layer:
- Data Access: Handle all Notion API interactions
- Consistent Response Format: Always return {success, data/error, timestamp}
- Raw Data: Return unfiltered data from external APIs
✅ Separation of Concerns:
- Controller: HTTP validation, routing, error responses, basic data cleanup
- Service: External API calls, data retrieval
- Client: Complex data processing, extraction, formatting
{ "success": true, "data": { "object": "page", "id": "notion-page-id", "properties": { "Name": { "type": "title", "title": [{"plain_text": "Demo Name"}] }, "Status": { "type": "select", "select": {"name": "Active"} } // Button properties filtered out } }, "timestamp": "2025-07-15T17:25:00.000Z" }
Add demo page links to dashboard for user-friendly testing with real Notion page IDs
// Update /backend/routes/views/dashboard.tsx imports and data fetching
import { getHealthStatus } from "../../controllers/health.controller.ts";
import { getDatabasePages } from "../../services/notion.service.ts";
export default async (c) => {
const userEmail = c.get("userEmail");
// Fetch health status for display
const healthData = await getHealthStatus();
// Fetch demo database pages for glimpse endpoints
const demoDatabaseId = Deno.env.get("GLANCE_DEMOS_DB_ID");
let demoPages = [];
let demoPagesError = null;
if (demoDatabaseId) {
const demoPagesResult = await getDatabasePages(demoDatabaseId);
if (demoPagesResult.success) {
demoPages = demoPagesResult.data.results || [];
} else {
demoPagesError = demoPagesResult.error;
}
}
Add this section after the webhook section closes, before the main content div closes:
<div class="webhook-section">
<h3>🔍 Demo API Endpoints</h3>
${demoDatabaseId ? `
${demoPages.length > 0 ? `
<p>Test the <code>/views/glimpse/:id</code> endpoint with these demo pages:</p>
<div style="margin-bottom: 20px;">
${demoPages.map(page => {
const pageTitle = page.properties?.Name?.title?.[0]?.plain_text ||
page.properties?.Title?.title?.[0]?.plain_text ||
'Untitled Page';
const glimpseUrl = `${c.req.url.split('/')[0]}//${c.req.url.split('/')[2]}/views/glimpse/${page.id}`;
return `
<div style="margin-bottom: 10px;">
<a href="${glimpseUrl}" target="_blank" style="
display: inline-block;
background: #007bff;
color: white;
text-decoration: none;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
margin-right: 10px;
">📄 ${pageTitle}</a>
<code style="font-size: 12px; color: #6c757d;">${glimpseUrl}</code>
</div>
`;
}).join('')}
</div>
` : `
${demoPagesError ? `
<p style="color: #dc3545;">❌ Error loading demo pages: ${demoPagesError}</p>
` : `
<p style="color: #6c757d;">No demo pages found in the configured database.</p>
`}
`}
` : `
<p style="color: #6c757d;">Demo database not configured. Set <code>GLANCE_DEMOS_DB_ID</code> environment variable.</p>
`}
</div>
✅ Correct Approach:
- Data Source: Use notion.pages.retrieve() for page data
- Controller Logic: Keep minimal but include useful data cleanup
- Property Filtering: Remove UI-specific elements (buttons) that don't belong in data APIs
- Error Handling: Consistent service response pattern
✅ Appropriate Controller Processing:
- UI Element Removal: Filter out button properties (not data)
- Security Cleanup: Remove potentially sensitive UI configurations
- Performance: Reduce payload size by removing unnecessary properties
❌ Avoid These Mistakes:
- Over-processing: Don't extract/transform business data in controller
- Tight Coupling: Don't assume specific property names or structures
- Inconsistent Responses: Don't create custom response formats
- Mixed Responsibilities: Don't mix HTTP handling with complex business logic
- Clean Data: Removes UI clutter from data API responses
- Maintainable: Clear separation between layers with focused responsibilities
- Flexible: Client can still extract any business data from Notion response
- Consistent: Follows existing service response patterns
- Secure: Prevents exposure of UI configurations
- Performance: Smaller payloads without unnecessary UI properties
- Future-proof: Easy to extend filtering logic for other UI property types
- User-Friendly Testing: Dashboard provides real page IDs for immediate testing
✅ Good reasons to process in controller:
- Remove UI-specific elements (buttons, formulas for display)
- Filter sensitive configuration data
- Remove properties that are never useful for data consumption
- Basic security/privacy filtering
❌ Avoid processing in controller:
- Complex business logic transformations
- Data extraction that depends on specific use cases
- Heavy computational processing
- Application-specific data formatting
- This approach creates a clean, maintainable endpoint that provides useful data filtering while maintaining architectural best practices and keeping the controller focused on its core responsibilities.
Townie, stop here! Before proceeding to additional steps, confirm that this step is working correctly.
If everything is working as intended: conclude this step with these messages:
- Briefly summarize the work that was just completed and why it's important
- Provide the URL to the dashboard where users can see the live demo API endpoint links. Tell the user that they can copy and paste that URL into a new tab to see it live.
- Explain that users can click on the demo page links in the dashboard to test
the
/views/glimpse/:id
endpoint with real Notion page data
Key Benefits Achieved:
- Users get real Notion page IDs for testing (not fake
/test-id
) - One-click access to live API responses from the dashboard
- Self-documenting API with automatically updated demo links
- Proper architectural separation maintained
Tell the user that the next step is to add the /glimpse
endpoint, which will
call the same controller as the endpoint they just created at /views/glimpse
.
Lastly, tell the user to copy this line and paste it into Townie:
Add a route for
/glimpse/:id
by following the instructions in/_townie/05-glimpse.md