Skip to content

Controllers and views

A controller is server code. A view is the React page that calls it. The pair is typed end to end: the client knows the controller’s method signatures, so a changed parameter breaks the build rather than production.

Subclass PlatformController and add public async methods. Each becomes callable from a view.

import { PlatformController, sql, tableRef } from "platform:stdlib";
import { Issue, User } from "platform:entities";
const i = tableRef(Issue);
const u = tableRef(User);
export default class IssueController extends PlatformController {
async getIssueDetail({ recordId }: { recordId: string }) {
const { rows } = await sql.query<Issue>(
`SELECT ${i.issue_id}, ${i.title}, ${u.full_name}
FROM ${i}
JOIN ${u} ON ${u.user_id} = ${i.assignee_id}
WHERE ${i.record_id} = $1`,
[recordId],
);
return rows[0] ?? null;
}
}

Methods take a single object argument and return a promise. That shape is what makes the generated client type work.

Errors are caught and returned as { error: message } rather than crashing the page, so a view should handle a result that is not the shape it expected.

A view is a React component. Build a typed client with createController, parameterized by the controller’s type — imported with import type, so no server code ships to the browser.

import React, { useEffect, useState } from "react@19.2.5";
import { Stack, Typography } from "@mui/material@7.3.10";
import { createController } from "platform:stdlib";
import type IssueController from "../controllers/IssueController.ts";
const controller = createController<IssueController>();
export default function IssueView({ recordId }: CustomViewProps) {
const [issue, setIssue] = useState<Awaited<
ReturnType<IssueController["getIssueDetail"]>
> | null>(null);
useEffect(() => {
controller.getIssueDetail({ recordId }).then(setIssue);
}, [recordId]);
if (!issue) return <Typography>Loading…</Typography>;
return (
<Stack spacing={2}>
<Typography variant="h5">{issue.title}</Typography>
</Stack>
);
}

Imports carry a version (react@19.2.5, @mui/material@7.3.10) so a page pins what it was written against and upgrades on your schedule.

ControllerClient<T> is the underlying type: it maps the controller’s methods, dropping run, and preserves each signature.

guest_files/
controllers/
IssueController.ts
views/
issueView.tsx

Deploy with polymesa deploy ./config, then attach the view to an entity as its view or create page. See Pages.