Utilities
A reference for the built-in helper utilities Thunder ships under @/core/utils.
Thunder ships with a set of production-ready helpers under @/core/utils/. This page covers the standalone utilities. Some larger utilities have their own pages: createCRUD, Caching, Logger, and the Env class.
Prefer Built-in Utilities (Don't Reinvent or Over-install)
Before writing helper code or reaching for a third-party/npm package, check whether Thunder already ships the capability under @/core/utils/ (and the HTTP helpers under @/core/http/). Reuse the built-in instead of writing your own copy or installing a redundant dependency.
Built-ins that already exist - do not re-implement these:
| Capability | Use this |
|---|---|
| Env management | Env from @/core/utils/env.ts (don't read Deno.env directly or add dotenv libs) |
| Logging | Logger from @/core/utils/logger.ts |
| Caching | cache from @/core/utils/cache.ts |
| Hashing | createHash family from @/core/utils/hash.ts |
| Query parsing | queryAsJson / parseQueryParams |
| Pagination | paginated from @/core/utils/paginate.ts |
| Template rendering | ejsRender / handlebarsRender from @/core/utils/textRender.ts |
| CRUD endpoints | createCRUD and $objectId from @/core/utils/createCRUD.ts |
| Request parsing & auth | paramsAsJson, bodyAsJson, parseBasicAuth, serveAssets from @/core/http/utils.ts |
| Responses | Response helpers from @/core/http/response.ts |
| Validation | zod (already a dependency) |
Rules of thumb:
- Search the framework first. If a utility exists, use it - never hand-roll a duplicate or pull in an external library for something already provided.
- Only add a dependency for a genuine, unmet need (e.g. a third-party SDK like Stripe), and justify it.
- Prefer Deno std (
@std/*) already mapped indeno.jsonover arbitrary npm packages when a standard option exists. - If a built-in is insufficient, prefer composing/wrapping it in your own module over replacing it.
denoConfig
Provides access to your project's deno.json configuration at runtime - useful for reading project metadata, import maps, tasks, or custom config values.
denoConfig is an already-resolved object (loaded at module initialization), not a function. Do not call it.
import { denoConfig, readDenoConfig, denoConfigPath } from "@/core/utils/denoConfig.ts";
console.log(denoConfig.name); // your project name
console.log(denoConfig.tasks); // configured deno tasks
// Read a different config file explicitly:
const other = await readDenoConfig("./plugins/foo/deno.json");
// Pass `true` as the second arg to read statically (JSON.parse, no import cache):
const fresh = await readDenoConfig(denoConfigPath, true);Template Rendering
Render EJS or Handlebars templates to strings. Both functions take the template content as a string (not a file path) plus a data object. ejsRender is async; handlebarsRender is sync.
import { ejsRender, handlebarsRender } from "@/core/utils/textRender.ts";
const html = await ejsRender("<h1>Hello <%= name %></h1>", { name: "Alice" });
const greeting = handlebarsRender("Hello {{name}}", { name: "Bob" });
// To render a file, read it first:
const tpl = await Deno.readTextFile("templates/welcome.ejs");
const out = await ejsRender(tpl, { name: "Alice", appName: "Thunder App" });
return new Response(html, { headers: { "Content-Type": "text/html" } });Hashing
One-way hashing built on Web Crypto (async) and Node crypto (sync). These produce digests only.
There is no verify helper - compare digests yourself. These are also not suitable for password storage on their own (no salting); use a dedicated KDF for passwords.
import {
createHash, // async, hex output
createHashBase64, // async, base64url output
createHashSync, // sync, hex output
createHashBase64Sync, // sync, base64url output
SupportedHashAlg,
} from "@/core/utils/hash.ts";
const hex = await createHash(SupportedHashAlg.SHA_256, "data");
const b64 = await createHashBase64(SupportedHashAlg.SHA_256, "data");
const hexSync = createHashSync(SupportedHashAlg.SHA_256, "data");
// Manual verification:
const isMatch = (await createHash(SupportedHashAlg.SHA_256, input)) === stored;Logger
A structured logging utility with colour-coded output for each severity level. Use Logger instead of console.log to maintain consistent, readable log output across your application.
import { Logger } from "@/core/utils/logger.ts";
Logger.success("Server started");
Logger.info("Connecting to database...");
Logger.warn("Rate limit approaching");
Logger.error("Failed to connect", error);| Method | Use Case |
|---|---|
Logger.success(msg) | Successful operations (e.g. startup, insert) |
Logger.info(msg) | General informational messages |
Logger.warn(msg) | Non-critical warnings |
Logger.error(msg, err?) | Errors and exceptions |
Only errors are logged in production mode.
parseQueryParams
Parses a query string into a nested object via qs. It takes a string (not a Request).
import { parseQueryParams } from "@/core/utils/parseQueryParams.ts";
const params = parseQueryParams("?page=1&filter[status]=active");
// { page: "1", filter: { status: "active" } }Inside route handlers, prefer queryAsJson(req) from @/core/http/utils.ts - it wraps parseQueryParams for requests and pairs naturally with Zod via $query.parse(queryAsJson(req)). See Routes.
paginated
Repeatedly invokes a callback in fixed-size pages until a page returns fewer items than limit (i.e. the last page). Useful for batch-processing an entire collection without loading it all into memory at once.
import { paginated } from "@/core/utils/paginate.ts";
await paginated(100, async (offset, limit) => {
const batch = await collection.find().skip(offset).limit(limit).toArray();
for (const doc of batch) {
// process each doc...
}
return batch.length; // loop continues while this equals `limit`
});See Also
These utilities also live under @/core/utils/ but have dedicated pages:
- createCRUD and
$objectId(createCRUD.ts) - Caching -
cache(cache.ts) - Environment Variables - the
Envclass (env.ts)