Skip to content

Configuration & secrets

Having a flexible configuration setup enables you to easily change certain behaviour in different environments.

Defining your profiles

First of all, we recommended you to define separate profiles for your environments.

For example, profiles for local environments:

  • LOCAL_DEV for local development
  • LOCAL_TEST for running integrations tests (locally, or in your pipeline)

Or, profiles for AWS environments:

  • AWS_DEV for the AWS development environment (also known as "TEST")
  • AWS_STG for the AWS staging environment (also known as "ACC")
  • AWS_PRD for the AWS production environment

To activate the correct profile, you could use a single environment variable (e.g. APP_PROFILE).

This prevents a complicated setup with many different environment variables that may clutter your infrastructure code.

typescript
type AppProfile =
  | "LOCAL_DEV"
  | "LOCAL_TEST"
  | "AWS_DEV"
  | "AWS_STG"
  | "AWS_PRD";

export function detectProfile(): AppProfile {
  const profileFromEnv = process.env.APP_PROFILE?.toUpperCase();

  switch (profileFromEnv) {
    case "LOCAL_DEV":
    case "LOCAL_TEST":
    case "AWS_DEV":
    case "AWS_STG":
    case "AWS_PRD":
      return profileFromEnv;
    case undefined:
      return "LOCAL_TEST"; // default profile
    default:
      throw new Error(`Unknown profile ${profileFromEnv}`);
  }
}

Make sure to configure this environment variable in your AWS Lambda CDK construct:

ts
new NodejsFunction(this, id, {
  // ...
  environment: {
    APP_PROFILE: `AWS_${this.environmentName.toUpperCase()}`
  }
});

Define your app config schema

Next, define a schema for your configuration. You can use either a simple TypeScript interface, or a Zod schema.

For example, using a TypeScript interface:

typescript
export interface AppConfig {
  dynamoDb: {
    myTableName: string;
    endpoint?: string;
  };
  clients: {
    disruptions: {
      baseUrl: string;
      apiKey: string;
      timeoutMilliseconds: number;
    };
  };
}

Having a single configuration object allows you to maintain all your application configuration in a single place, with the ability to structure it to your needs.

Define and load your configs

A simple way load the right configuration file for a given profile, is to define them as regular ECMAScript modules and load them dynamically using dynamic imports.

This makes sure the code is only evaluated when a certain profile is active. This is very powerful, since it allows you to load secrets from AWS in one profile, but not in another.

For example:

typescript
import type { AppProfile } from "./app-profile.ts";
import type { AppConfig } from "./app-config.ts";

/**
 * Loads the app configuration for a specific profile.
 */
async function loadAppConfig(profile: AppProfile): Promise<AppConfig> {
  const configProvider = await configProviders[profile];

  return configProvider();
}

const configProviders: Record<AppProfile, Promise<AppConfig>> = {
  LOCAL_DEV: import("./app-config.local-dev.ts").then((it) => it.LOCAL_DEV),
  AWS_DEV: import("./app-config.aws-dev.ts").then((it) => it.AWS_DEV),
  // ...
};
typescript
import type { AppConfig } from "./app-config";

/**
 * Config file for local integration testing, works offline with Docker.
 */
export const LOCAL_DEV: AppConfig = {
  dynamoDB: {
    // for this profile, we rely on an fixed table name, but we use a DynamoDB running in Docker
    myTableName: "MyTableName",
    endpoint: "http://localhost:4567",
  },
  clients: {
    disruptions: {
      // for example, use a mocked endpoint:
      baseUrl: "http://localhost:8080/disruptions",
      timeoutMilliseconds: 350,
      apiKey: "",
    },
  },
};
typescript
import type { AppConfig } from "./app-config";
import { getAppSecrets } from "./app-secrets";

// for this profile, we want to load secrets from AWS
const secrets = await getAppSecrets();

export const AWS_DEV: AppConfig = {
  dynamoDB: {
    // for this profile, we rely on an environment variable
    myTableName: process.env.MY_TABLE_NAME,
  },
  clients: {
    disruptions: {
      baseUrl: "https://gateway.apiportalacc.ns.nl/disruptions",
      timeoutMilliseconds: 350,
      apiKey: secrets.disruptionsAPIKey,
    },
  },
};

Looking for an example?

Have a look at the Bookstore REST API demo, which also uses this setup.