This guide walks through how to add a new browser-side feature to COLD, covering:
browser_api.ts and cold_api.yamlThe existing rdm_review_queue feature is used throughout
as the reference example, since it demonstrates the full pattern
end-to-end.
TypeScript source (.ts)
└─→ deno task htdocs (runs build.ts + deno bundle)
└─→ htdocs/modules/*.js (served as static files)
└─→ browser imports via <script type="module">
└─→ calls /api/{collection}/{query} (browser_api.ts)
└─→ datasetd (cold_api.yaml SQL)
The browser never talks to datasetd directly. It calls
COLD’s read-only /api/* endpoint, which proxies
parameterized SQL queries defined in cold_api.yaml.
Create a .ts file in the project root (alongside the
other source files like rdm_review_queue.ts). The file
should export a class or functions that the HTML page will import.
// my_feature.ts
import { ClientAPI } from "./client_api.ts";
export class MyFeatureUI {
private searchElement: HTMLElement;
private resultSection: HTMLElement;
private clientAPI: ClientAPI;
constructor(options: { searchElement: string | HTMLElement; baseUrl: URL }) {
this.searchElement =
typeof options.searchElement === "string"
? (document.getElementById(options.searchElement) as HTMLElement)
: options.searchElement;
this.clientAPI = new ClientAPI(options.baseUrl.toString());
// Build the UI by injecting HTML into the target element
this.searchElement.innerHTML = `
<form>
<input id="q" name="q" type="search" placeholder="Search..." size="40">
<input type="submit" value="Search">
<input type="reset" value="Clear">
</form>
<section id="my-feature-results"></section>
`;
this.resultSection = this.searchElement.querySelector("section")!;
this.searchElement.querySelector("form")!.addEventListener("submit", async (e) => {
e.preventDefault();
const q = (this.searchElement.querySelector("#q") as HTMLInputElement).value.trim();
await this.runQuery(q);
});
}
private async runQuery(q: string): Promise<void> {
const params = new URLSearchParams({ q });
// "people.ds" is the collection name; "lookup_by_something" is the SQL query name
const results = await this.clientAPI.getList("people.ds", "lookup_by_something", params);
this.resultSection.innerHTML = results.length
? results.map((r) => `<p>${r.clpid}: ${r.family_name}, ${r.given_name}</p>`).join("")
: "<p>No results.</p>";
}
}client_api.ts, orcid_api.ts, and
directory_client.ts are the approved browser-side
dependencies.fetch(),
document, HTMLElement,
URLSearchParams, URL,
window.history, DOMContentLoaded. These are
all available without imports..js file.DOMContentLoaded.build.tsbuild.ts lists the TypeScript files that
@deno/emit transpiles individually to
htdocs/modules/. Open build.ts and add your
file to the transpileFiles array:
// build.ts (existing code, abbreviated)
let transpileFiles = [
"client_api.ts",
"orcid_api.ts",
"directory_client.ts",
"my_feature.ts", // ← add your file here
];The transpiler will output
htdocs/modules/my_feature.js.
deno bundle insteaddeno bundle is used for modules that have
cross-file imports that must be merged into one output
file (e.g., rdm_review_queue.ts imports
client_api.ts). If your module only imports from
client_api.ts and both files will be loaded on the same
page, you can let the browser resolve the import at runtime — just add
your file to transpileFiles in build.ts.
If you need a single self-contained bundle (no runtime import
resolution), add a deno bundle command to the
htdocs task in deno.json:
"htdocs": "deno run --allow-net --allow-read --allow-env --allow-write=htdocs --allow-import build.ts ; deno bundle --config tsconfig.json client_api.ts my_feature.ts --outdir htdocs/modules/"Check the existing htdocs task in deno.json
for the exact flags already in use and follow that pattern.
deno task htdocsThis runs build.ts (which calls @deno/emit)
and any deno bundle commands in the task. The output
appears in htdocs/modules/.
cold_api.yamlIf your feature needs data from a collection that is not already
exposed by an existing query, add a named query to
cold_api.yaml.
cold_api.yaml configures datasetd. Each
collection section has a query block of named SQL
statements. The query name becomes part of the URL path used by
browser_api.ts.
# cold_api.yaml (excerpt showing the pattern)
collections:
- dataset: people.ds
query:
# existing queries ...
lookup_by_something: |
SELECT json_object(
'clpid', src->>'clpid',
'family_name', src->>'family_name',
'given_name', src->>'given_name'
) AS src
FROM people
WHERE json_extract(src, '$.family_name') LIKE ?
ORDER BY json_extract(src, '$.family_name')Important: datasetd passes URL query
parameters positionally to SQL ? placeholders. The order of
parameters in the URL must match the order of ? in the SQL.
See the existing queries in cold_api.yaml for reference
patterns using json_extract and LIKE.
After editing cold_api.yaml you must restart
datasetd (the cold_api task) for the change to
take effect.
browser_api.ts (if needed)browser_api.ts is the server-side handler for all
/api/* requests from the browser. For most new queries
no changes are required — the generic
parameter-forwarding logic handles them automatically.
The handler at handleBrowserAPI() in
browser_api.ts:
/api/{c_name}/{query_name}?param=valueqObj
dictionaryds.query(query_name, pList, body) where
pList is the list of parameter namesdatasetdSo calling /api/people.ds/lookup_by_something?q=Smith
automatically forwards q=Smith to the SQL query’s first
? placeholder — no code change needed.
browser_api.tsYou need to add special-case handling in browser_api.ts
when:
Parameter names need remapping — e.g., the
browser sends q but the SQL expects two separate parameters
(name and alternative). Look at the existing
lookup_clgid special case as a reference:
// browser_api.ts (existing special case)
if (apiReq.query_name === "lookup_clgid") {
body = JSON.stringify({ name: apiReq.q, alternative: apiReq.q });
pList = ["name", "alternative"];
}Add a similar else if block for your query name if it
needs the same treatment.
The collection name needs to be blocked — if you
need to prevent access to a particular collection or query from the
browser for security reasons, add an explicit check before the
ds.query() call.
Response transformation is required — if the raw
JSON from datasetd needs reshaping before the browser
receives it, add transformation logic after resp.json() and
before renderJSON().
// browser_api.ts — inside handleBrowserAPI(), before the generic else block
} else if (apiReq.query_name === "lookup_by_something") {
// SQL expects two positional params: first_name and last_name
// but the browser only sends a single "q" value
body = JSON.stringify({ first_name: apiReq.q, last_name: apiReq.q });
pList = ["first_name", "last_name"];
} else {
// existing generic path
for (let k of Object.keys(apiReq)) {
...
}
}ClientAPI (optional but
recommended)client_api.ts is the browser-side class that wraps
fetch() calls to /api/*. Adding a named method
makes the feature easier to use from your UI module and provides a typed
return value.
// client_api.ts — add inside the ClientAPI class
async lookupBySomething(
q: string,
): Promise<{ clpid: string; family_name: string; given_name: string }[]> {
const params = new URLSearchParams({ q });
return (await this.getList("people.ds", "lookup_by_something", params)) as {
clpid: string;
family_name: string;
given_name: string;
}[];
}Then in my_feature.ts use it:
const results = await this.clientAPI.lookupBySomething(q);After editing client_api.ts, re-run
deno task htdocs so the change is compiled into
htdocs/modules/client_api.js.
Create htdocs/my_feature.html. It should be a plain
HTML5 file. Use an existing page like
htdocs/rdm_review_queue.html or
htdocs/collaborator_report.html as a template.
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Feature - COLD</title>
<link rel="stylesheet" href="css/site.css">
</head>
<body>
<nav>
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
<header>
<h1>My Feature</h1>
</header>
<main>
<!-- The UI module injects its form and results into this element -->
<div id="my-feature-container"></div>
</main>
<footer>
<p>Caltech Library</p>
</footer>
<script type="module">
import { MyFeatureUI } from "./modules/my_feature.js";
// Resolve the base URL (strip the filename so relative API calls work)
const baseUrl = new URL(window.location.href);
baseUrl.pathname = baseUrl.pathname.replace(/my_feature\.html$/, "");
baseUrl.search = "";
window.addEventListener("DOMContentLoaded", () => {
new MyFeatureUI({
baseUrl: baseUrl,
searchElement: "my-feature-container",
});
});
</script>
</body>
</html>baseUrl
normalization?ClientAPI uses baseUrl to construct
/api/ URLs. If the page is served at
/my_feature.html, the browser’s
window.location.href is .../my_feature.html.
The module strips the filename so that the constructed API URL becomes
/api/people.ds/... rather than
/my_feature.htmlapi/people.ds/.... This matches the pattern
used by every other page in htdocs/.
Type-check — catches TypeScript errors before building:
deno task checkBuild browser modules:
deno task htdocsConfirm htdocs/modules/my_feature.js exists.
Start the backend (if not running):
deno task cold_api # starts datasetd on port 8112Start the middleware:
deno task cold # starts cold on port 8111, watches for changesOpen the page at
http://localhost:8111/my_feature.html and exercise the
feature.
Check the API directly to verify the query works:
http://localhost:8111/api/people.ds/lookup_by_something?q=Smith
Should return a JSON array.
| File | Change |
|---|---|
my_feature.ts |
New — browser-side UI module |
build.ts |
Add "my_feature.ts" to transpileFiles (or
add bundle command to deno.json) |
cold_api.yaml |
Add named SQL query to the relevant collection |
browser_api.ts |
Only if parameter remapping or response transformation is needed |
client_api.ts |
Optional — add a typed wrapper method for the new query |
htdocs/my_feature.html |
New — HTML page that loads the module |
The only file that must change is build.ts (or
deno.json). Everything else follows from whether your query
fits the generic path in browser_api.ts or needs special
handling.