· deepdives  · 6 min read

Demystifying the Portal API: A Deep Dive into Constants and Variables

Understand how to design, organize, and use constants and variables in the Portal API. This deep dive covers compile-time vs runtime values, TypeScript patterns, environment-driven configuration, security considerations, versioning, and practical code examples.

Understand how to design, organize, and use constants and variables in the Portal API. This deep dive covers compile-time vs runtime values, TypeScript patterns, environment-driven configuration, security considerations, versioning, and practical code examples.

Why this matters

When building integrations with any API - including a Portal API - the way you handle constants and variables influences reliability, security, maintainability, and developer experience. Constants represent fixed pieces of information (routes, header names, event keys, error codes), while variables capture runtime state (auth tokens, user IDs, environment-specific configuration). Getting the separation and patterns right reduces bugs and makes updates painless.

What is the Portal API (briefly)

For this article, “Portal API” refers to a typical service API used by web and mobile apps: it exposes endpoints, holds resource identifiers, emits events/webhooks, and requires authentication. The patterns below are general but grounded in real-world needs: endpoint versioning, feature flags, environment-specific configuration, and secure handling of secrets.

Constants vs Variables - definitions and when to use each

  • Constants: values that do not change while the application runs. Examples: API route patterns (“/v1/users”), header names (“X-Portal-Event”), known status codes, enumerated event types.
  • Variables: values that can change at runtime. Examples: access tokens, current user ID, connection status, and values read from environment variables that differ between deployments.

Use constants for identity, contract, and domain vocabulary. Use variables where runtime or environment-specific values are required.

References: MDN: const

Organizing constants - patterns that scale

  1. Group by domain
  • apiRoutes.js / apiRoutes.ts
  • headers.ts
  • events.ts
  • errorCodes.ts
  1. Name consistently
  • Uppercase with underscores for constants (API_BASE_URL)
  • PascalCase or enum style for domain enums (UserRole.Admin)
  1. Avoid duplication

Centralize the canonical values so one change propagates.

Example file structure:

src/
  config/
    apiRoutes.ts
    headers.ts
    errorCodes.ts
  services/
  components/

Practical examples

1) Simple JS: grouping API routes and headers

// src/config/apiRoutes.js
export const API_VERSION = 'v1';
export const API_BASE = `/api/${API_VERSION}`;

export const ROUTES = Object.freeze({
  USERS: `${API_BASE}/users`,
  AUTH: `${API_BASE}/auth`,
  PORTAL_STATS: `${API_BASE}/portal/stats`,
});

// src/config/headers.js
export const HEADERS = Object.freeze({
  AUTH: 'Authorization',
  REQUEST_ID: 'X-Request-Id',
  PORTAL_EVENT: 'X-Portal-Event',
});

// usage
import { ROUTES, HEADERS } from './config/apiRoutes';
fetch(ROUTES.USERS, { headers: { [HEADERS.AUTH]: `Bearer ${token}` } });

Notes: using Object.freeze ensures runtime immutability for top-level fields. For deep immutability, see the deep-freeze pattern below.

2) TypeScript: enums, const assertions, and typed config

TypeScript gives you stronger contracts for API constants.

// src/config/events.ts
export const PortalEvent = {
  USER_CREATED: 'portal:user.created',
  USER_UPDATED: 'portal:user.updated',
  PORTAL_SYNC: 'portal:sync',
} as const;

export type PortalEvent = (typeof PortalEvent)[keyof typeof PortalEvent];

// Usage
function handleEvent(event: PortalEvent) {
  if (event === PortalEvent.USER_CREATED) {
    // typed and safe
  }
}

You can also use enums for numeric/constants, but string const assertions provide better interoperability with JSON and external services.

References: TypeScript const assertions

3) Runtime configuration: environment variables and defaults

Never hardcode environment-specific values into constants that end up in the repository (especially secrets). Use environment variables for things that vary per deployment.

// src/config/index.js
import dotenv from 'dotenv';
dotenv.config();

export const CONFIG = Object.freeze({
  API_BASE_URL: process.env.PORTAL_API_BASE_URL || 'https://api.example.com',
  TIMEOUT_MS: Number(process.env.PORTAL_TIMEOUT_MS) || 10_000,
  FEATURE_FLAGS: {
    NEW_DASHBOARD: process.env.FEATURE_NEW_DASHBOARD === 'true',
  },
});

In a TypeScript project, you can declare types for process.env with @types/node or create a typed config builder.

References: dotenv, Node.js process.env

4) Secure handling: proxies and secrets

  • Do not put secrets (API private keys, DB passwords) into client-side constants.
  • For web apps, keep secrets on server-side or use a token exchange flow so the browser never sees raw secrets.
  • Use secret stores (AWS Secrets Manager, HashiCorp Vault) for backend config.

OWASP guidance: Sensitive Data Exposure

Advanced patterns and tips

Deep-freeze a constant object

If you want true immutability:

function deepFreeze(obj) {
  Object.getOwnPropertyNames(obj).forEach(name => {
    const prop = obj[name];
    if (typeof prop === 'object' && prop !== null) {
      deepFreeze(prop);
    }
  });
  return Object.freeze(obj);
}

const CONFIG = deepFreeze({
  api: { base: '/api/v1' },
  errors: { NOT_FOUND: 404 },
});

Use symbols for internal-only unique constants

export const INTERNAL = {
  REQUEST_ID: Symbol('requestId'),
};

Symbols are unique and cannot clash with strings, but they are not serializable.

Feature flags and toggles

Use boolean configuration flags for gradual rollouts. Store flags as env vars, a remote feature-flag store, or a config service.

Compile-time constants for client bundles

When building single-page apps, use your bundler’s define plugin (Vite import.meta.env, Webpack DefinePlugin) to inject build-time constants. Example with Vite:

// vite.config.js
export default defineConfig({
  define: { __BUILD_TIME__: JSON.stringify(Date.now()) },
});

Be careful: BUILD_TIME and other define’d constants are baked into client bundles and cannot be changed at runtime.

Versioning and backward compatibility

  • Keep API version strings as constants (API_VERSION = ‘v1’) and reference them across your codebase.
  • When deprecating constants (like a route), keep the old constant for a transition period and add a migration note.
  • Treat contract changes (names of events, error codes) as breaking changes; bump API major version according to semver: https://semver.org

Testing with constants

  • Use constants in tests to avoid brittle string duplication.
  • When tests need to simulate different environments, inject a config factory that can be swapped for test values.

Example: factory injection

// src/config/factory.ts
export function createConfig(env = process.env) {
  return {
    apiBase: env.PORTAL_API_BASE_URL || 'http://localhost:3000',
  } as const;
}

// in tests
const testConfig = createConfig({ PORTAL_API_BASE_URL: 'http://test:3000' });

Common pitfalls and how to avoid them

  • Committing secrets to constants: use secret management and never commit .env with secrets.
  • Overusing constants: not every value needs to be a top-level constant. Keep constants for values that are part of the app’s contract or repeated across code.
  • Scattering values: centralize domain constants to avoid inconsistent naming.
  • Using mutable objects for constants: freeze them or use const assertions in TypeScript.

Example: Building a small Portal API client

// src/client/portalClient.ts
import fetch from 'node-fetch';
import { CONFIG } from '../config';
import { ROUTES, HEADERS } from '../config/apiRoutes';

export async function getUsers(token: string) {
  const res = await fetch(`${CONFIG.API_BASE_URL}${ROUTES.USERS}`, {
    headers: { [HEADERS.AUTH]: `Bearer ${token}` },
  });
  if (!res.ok) throw new Error(`Portal API error: ${res.status}`);
  return res.json();
}

This pattern cleanly separates where values come from: ROUTES and HEADERS are constants defining the API contract; CONFIG.API_BASE_URL is deployment-specific.

Migration checklist for changing a constant (safe rollout)

  1. Add the new constant alongside the old one.
  2. Update internal callers to reference the new constant.
  3. Run tests and rollout server change.
  4. Keep both constants for a deprecation window to support older clients.
  5. Remove the old constant after clients are migrated and after appropriate versioning/announcements.

Summary - core takeaways

  • Use constants for API contracts (routes, headers, event names, error codes) and variables for runtime state.
  • Group, freeze, and type constants to avoid accidental mutation or duplication.
  • Use environment variables for deployment-specific configuration; never bake secrets into client-side constants.
  • Prefer TypeScript const assertions for string unions and enums for numeric or closed-domain sets.
  • Plan and version changes to constants as breaking changes when they alter the API contract.

Further reading

Back to Blog

Related Posts

View All Posts »
Mastering the Contact Picker API: A Step-by-Step Guide

Mastering the Contact Picker API: A Step-by-Step Guide

A comprehensive tutorial on the Contact Picker API: feature detection, implementation patterns, TypeScript examples, fallbacks, privacy/security best practices, and testing tips to build a smooth, privacy-first contact selection flow.