Configuration (typed settings)¶
Every service reads configuration from environment variables. Instead of
scattering process.env.X ?? "default" across the code, the SDK offers
composable settings fragments — objects of Zod fields, one per domain (JWT,
email, Redis…) — that you combine into your service schema and validate in one
shot with loadSettings.
It's the faithful port of the tempest-fastapi-sdk settings mixins: the
same environment variable names and the same defaults.
Where z comes from
z is Zod already carrying .openapi(), re-exported by the SDK. Import
everything from tempest-express-sdk.
1. The basics: base + loadSettings¶
baseAppSettingsShape already ships server + database + CORS. loadSettings
parses process.env, applies the defaults and freezes the result:
import { baseAppSettingsShape, loadSettings, z } from "tempest-express-sdk";
export const settings = loadSettings(z.object(baseAppSettingsShape));
settings.HOST; // "127.0.0.1"
settings.PORT; // 8000
settings.DATABASE_URL; // "sqlite://./app.db"
settings.CORS_ORIGINS; // string[] (CSV → list)
A missing value falls back to the default; a malformed value (non-numeric port,
invalid enum) throws ZodError at boot — fail early, not in production. ✅
2. Composing domain fragments¶
Every dependency has a fragment. Spread (...) the ones the service uses inside
a single z.object:
import {
baseAppSettingsShape,
jwtSettingsShape,
redisSettingsShape,
emailSettingsShape,
loadSettings,
z,
} from "tempest-express-sdk";
export const settings = loadSettings(
z.object({
...baseAppSettingsShape,
...jwtSettingsShape,
...redisSettingsShape,
...emailSettingsShape,
// + your own service fields:
STRIPE_API_KEY: z.string(),
}),
);
settings.JWT_SECRET; // string
settings.REDIS_URL; // string
settings.SMTP_HOST; // string
settings.STRIPE_API_KEY;
The available fragments:
| Fragment | Variables (prefix) | For |
|---|---|---|
serverSettingsShape |
HOST, PORT, DEBUG |
Server bind. |
databaseSettingsShape |
DATABASE_URL |
Database connection. |
corsSettingsShape |
CORS_ORIGINS (CSV) |
CORS. |
logSettingsShape |
LOG_LEVEL, LOG_JSON, LOG_DIR |
Structured logging. |
jwtSettingsShape |
JWT_* |
JWT signing/verification. |
tokenSettingsShape |
TOKEN_SECRET |
Shared-secret guards (X-Token). |
authSettingsShape |
AUTH_* |
Signup/activation/reset/MFA flows + token delivery. |
emailSettingsShape |
SMTP_* |
SMTP transport. |
redisSettingsShape |
REDIS_URL |
Cache / sessions / SSE broker. |
rabbitmqSettingsShape |
RABBITMQ_* |
Queue broker. |
sessionSettingsShape |
SESSION_* |
Server-side sessions (cookie + TTL). |
uploadSettingsShape |
UPLOAD_* |
Local upload storage. |
minioSettingsShape |
MINIO_* |
MinIO/S3 object storage. |
webPushSettingsShape |
VAPID_*, WEBPUSH_* |
Web Push. |
webSocketSettingsShape |
WS_* |
WebSocket hub tuning. |
baseAppSettingsShape is already serverSettingsShape +
databaseSettingsShape + corsSettingsShape combined.
Fragments are pure objects
Each *Shape is just an object of Zod fields (as const) — nothing reads
the environment on its own. loadSettings reads process.env. That keeps
the fragments testable: z.object(jwtSettingsShape).parse({ JWT_SECRET: "..." }).
3. Booleans and lists from the environment¶
An environment variable is always a string. Two gotchas the SDK handles:
envBoolean — "false" is false¶
Zod's z.coerce.boolean() treats any non-empty string as true — so
"false" would become true. The fragments' boolean fields use envBoolean,
which reads the tokens on both sides and rejects what it does not recognise:
import { envBoolean, z } from "tempest-express-sdk";
const schema = z.object({ FEATURE_X: envBoolean(false) });
schema.parse({ FEATURE_X: "false" }).FEATURE_X; // false ✅
schema.parse({ FEATURE_X: "1" }).FEATURE_X; // true
schema.parse({ FEATURE_X: "" }).FEATURE_X; // false (empty = absent → default)
schema.parse({}).FEATURE_X; // false (default)
schema.parse({ FEATURE_X: "maybe" }); // ❌ ZodError
Becomes true |
Becomes false |
|---|---|
true 1 yes on y enabled |
false 0 no off n disabled |
An unknown token brings the boot down — on purpose
FEATURE_X=maybe raises a ZodError instead of becoming false. Wrong
environment config has to fail loudly: a typo that degrades into "used the
default" disappears from the log and shows up in production, on the wrong
day.
It is the same helper as looseBoolean
envBoolean is the looseBoolean from
Validated fields
under the settings domain's name — one implementation, shared with the query
filters, so the two can never drift apart.
envList — CSV becomes string[]¶
import { envList, z } from "tempest-express-sdk";
const schema = z.object({ TAGS: envList("a,b") });
schema.parse({ TAGS: "x, y ,, z" }).TAGS; // ["x", "y", "z"]
schema.parse({}).TAGS; // ["a", "b"]
Same mechanism as CORS_ORIGINS and the UPLOAD_ALLOWED_* fields.
4. A complete service¶
Inject the settings object wherever you need it — it's immutable, so it can be
a module singleton:
// src/core/settings.ts
import {
baseAppSettingsShape,
jwtSettingsShape,
loadSettings,
sessionSettingsShape,
z,
} from "tempest-express-sdk";
export const settings = loadSettings(
z.object({
...baseAppSettingsShape,
...jwtSettingsShape,
...sessionSettingsShape,
}),
);
export type Settings = typeof settings;
// anywhere
import { JWTUtils } from "tempest-express-sdk";
import { settings } from "@/core/settings";
const jwt = new JWTUtils(settings.JWT_SECRET, {
defaultTtlSeconds: settings.JWT_ACCESS_TTL_SECONDS,
algorithm: settings.JWT_ALGORITHM as "HS256",
});
Change the default secrets
JWT_SECRET and the VAPID keys ship with placeholders just so the app boots
in dev. In production, set them via the environment — never commit real
secrets.
Recap¶
baseAppSettingsShape+loadSettings(z.object(...))= validated, frozen configuration at boot.- Compose domain fragments with
...spread; each uses thetempest-fastapi-sdkenv names and defaults. envBoolean(the same helper aslooseBoolean) fixes boolean parsing and rejects an unknown token;envListturns CSV into a list.- An invalid value fails at boot, not in production. 🚀