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.