How to Structure JSON Configuration Files

JSON is a common choice for configuration files (as we’ve seen in the posts about package.json and tsconfig.json), but how you organize that file makes a big difference as the project grows. This post covers practical patterns for keeping configuration files readable and easy to maintain.

Grouping by context, not by data type

A common mistake is organizing configuration “flat,” with no grouping.

// json
{
  "dbHost": "localhost",
  "dbPort": 5432,
  "redisHost": "localhost",
  "redisPort": 6379,
  "apiTimeout": 5000,
  "apiUrl": "https://api.example.com"
}

This works fine for a few items, but gets hard to navigate as it grows. Grouping by context (nesting by service/domain) keeps things organized.

// json
{
  "database": {
    "host": "localhost",
    "port": 5432
  },
  "redis": {
    "host": "localhost",
    "port": 6379
  },
  "api": {
    "url": "https://api.example.com",
    "timeout": 5000
  }
}

This also makes it easier to access an entire block of configuration at once in your code (config.database, for example), instead of several loose keys.

Separating configuration by environment

A common pattern is having separate files per environment, instead of mixing everything into one with conditionals.

config/
  default.json
  development.json
  production.json
// config/default.json
{
  "api": {
    "timeout": 5000
  }
}
// config/production.json
{
  "api": {
    "timeout": 10000
  },
  "logLevel": "error"
}

The code loads the default.json file and then overrides it with the file specific to the current environment, usually merging the two objects.

// javascript
const defaults = require('./config/default.json');
const environment = require(`./config/${process.env.NODE_ENV}.json`);

const config = { ...defaults, ...environment };

Never put secrets directly in the configuration file

A serious and recurring mistake: putting passwords, API keys, or tokens directly in the configuration JSON, which usually goes into version control.

// Avoid this
{
  "database": {
    "password": "mypassword123"
  }
}

The recommended pattern is to use environment variables for any sensitive data, leaving only the structure/reference in the JSON.

{
  "database": {
    "passwordEnv": "DB_PASSWORD"
  }
}

Or, more commonly, not including the field in the JSON at all and reading the environment variable directly in code, using the JSON only for non-sensitive configuration.

Documenting the expected structure

Since plain JSON doesn’t support comments, a common practice is keeping a separate file (config.example.json or a README) documenting what each key expects.

// config.example.json
{
  "api": {
    "url": "Base URL for the external API",
    "timeout": "Timeout in milliseconds (number)"
  }
}

This helps whoever joins the project later understand quickly what each field does, without needing to read the code that consumes the configuration.

Validating the configuration at startup

A configuration error (a missing field, wrong type) should ideally fail fast, at application startup, instead of causing a confusing error later during execution. Using JSON Schema (already covered in another post) to validate the configuration file as soon as the application starts is a practice that saves a lot of headaches.

// javascript
const Ajv = require('ajv');
const ajv = new Ajv();

const schema = {
  type: "object",
  properties: {
    api: {
      type: "object",
      properties: {
        url: { type: "string" },
        timeout: { type: "number" }
      },
      required: ["url"]
    }
  }
};

const validate = ajv.compile(schema);
const isValid = validate(config);

if (!isValid) {
  console.error("Invalid configuration:", validate.errors);
  process.exit(1);
}

Quick checklist

  • Is the configuration grouped by context/service, instead of flat?
  • Are secrets (passwords, tokens, API keys) kept out of the JSON, coming from environment variables instead?
  • Is there an example file or documentation explaining what each key expects?
  • Does the application validate the configuration at startup, instead of failing silently later?

Before committing a configuration file, it’s worth confirming the syntax is correct. Paste it into JSON Validator to check it quickly.