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 usual tokens (true/1/yes/on) and treats the rest as
false:
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; // false (default)
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. envBooleanfixes boolean parsing;envListturns CSV into a list.- An invalid value fails at boot, not in production. 🚀