A common dilemma in any JavaScript project, be it React, Next.js, Node.js or other frameworks, is finding a way to handle constants. Avoiding the problem and simply hard-coded their values into the codebase leads to code duplication and maintainability issues. You would just end up spreading the same strings and values all over your project. Exporting these values to local const
variables is also of little benefit, as it does not promote code reusability.
How to organize constants in JavaScript? With a dedicated constants layer that stores all your constant values in the same place! In this article, you will discover why you need this layer in your JavaScript architecture and see three different ways to implement it.
Take your JavaScript constants management to the next level!
Why You Need to Structure Constants in JavaScript
A constants layer is the part of a frontend or backend application that is responsible for defining, managing, and exposing all the project’s constants. To understand why to add it to your JavaScript architecture, letβs consider an example.
Suppose you have a Node.js application with a few endpoints. When something goes wrong, you want to handle the errors and return human-readable messages. This is a common practice to avoid exposing implementation details to the frontend for security reasons.
Your Node.js application may look like this:
import express from 'express'
import { PlayerService } from './services/playerService'
const app = express()
// get a specific player by ID
app.get('/api/v1/players/:id', (req, res) => {
const playerId = parseInt(req.params.id)
// retrieve the player specified by the ID
const player = PlayerService.get(playerId)
// if the player was not found,
// return a 404 error with a generic message
if (player) {
return res.status(404).json({ message: 'Entity not found!' })
}
res.json(player)
})
// get all players with optional filtering options
app.get('/api/players', (req, res) => {
let filters = {
minScore: req.query.minScore,
maxScore: req.query.minScore
}
if ((minScore !== undefined && minScore < 0) || (maxScore !== undefined && maxScore < 0) || minScore > maxScore) {
return res.status(400).json({ message: 'Invalid query parameters!' })
}
const filteredPlayers = PlayerService.getAll(filters)
res.json(filteredPlayers)
})
// other endpoints...
// start the server
app.listen(3000, () => {
console.log(`Server running on port ${port}`)
})
As you can see, the above endpoints perform specific controls to ensure they can fulfill the request. Otherwise, they return error messages stored in inline strings. In a simple application, this approach is totally fine.
Now, imagine that your Node.js backend grows big. It starts to involve dozens of endpoints, structured in a multi-layered architecture. In this scenario, you are likely to spread error messages throughout your application. This is bad for three main reasons:
- Code duplication: The same error messages will appear several times in your codebase. If you want to change one of them, you will have to update all its occurrences, which leads to maintenance issues.
- Inconsistencies: Different developers might use different messages to describe the same problem. This can lead to confusion for the consumers of your API endpoints.
- Scalability issues: As the number of endpoints grows, the number of error messages increases linearly. When there are many files involved, it becomes progressively challenging to keep track of these messages.
You can easily generalize this example to any scenario where you need to repeat the same string, number, or object values in your code. But think about these values. What are they really? They are constant values and must be treated as such!
So, the first approach you might think of is to export them locally or at the file level intoΒ const
Β variables. The sample Node.js application would become:
import express from 'express'
import { PlayerService } from './services/playerService'
const app = express()
// the error message constants
const ENTITY_NOT_FOUND_MESSAGE = 'Entity not found!'
const INVALID_QUERY_PARAMETERS_MESSAGE = 'Invalid query parameters!'
// get a specific player by ID
app.get('/api/v1/players/:id', (req, res) => {
const playerId = parseInt(req.params.id)
// retrieve the player specified by the ID
const player = PlayerService.get(playerId)
// if the player was not found,
// return a 404 error with a generic message
if (player) {
return res.status(404).json({ message: ENTITY_NOT_FOUND_MESSAGE })
}
res.json(player)
})
// get all players with optional filtering options
app.get('/api/players', (req, res) => {
let filters = {
minScore: req.query.minScore,
maxScore: req.query.minScore
}
if ((minScore !== undefined && minScore < 0) || (maxScore !== undefined && maxScore < 0) || minScore > maxScore) {
return res.status(400).json({ message: INVALID_QUERY_PARAMETERS_MESSAGE })
}
const filteredPlayers = PlayerService.getAll(filters)
res.json(filteredPlayers)
})
// other endpoints...
// start the server
app.listen(3000, () => {
console.log(`Server running on port ${port}`)
})
This is better, but still not ideal. You solved the problem locally, but what if your architecture involved many files? You would experience the same drawbacks highlighted before.
What you need to focus on is that constants are designed to be reused. The best way to promote constants reusability in your code is to organize all JavaScript constants in a centralized layer. Only by adding a constants layer to your architecture can you avoid the duplication, consistency, and scalability issues.
Time to look at how to implement such a layer!
3 Approaches for Organizing Constants in a JavaScript Project
Let’s see three different approaches to building a layer for your constants in a JavaScript application.
Approach #1: Store all constants in a single file
The simplest approach to handling constants in JavaScript is to encapsulate them all in a single file. Create a constants.js
file in the root folder of your project and add all your constants there:
// constants.js
// API constants
export const GET = 'GET'
export const POST = 'POST'
export const PUT = 'PUT'
export const PATCH = 'PATCH'
export const DELETE = 'DELETE'
export const BACKEND_BASE_URL = 'https://example.com'
// ...
// error message constants
export const ENTITY_NOT_FOUND_MESSAGE = 'Entity not found!'
export const INVALID_QUERY_PARAMETERS_MESSAGE = 'Invalid query parameters!'
export const AUTHENTICATION_FAILED_MESSAGE = 'Authentication failed!'
// ...
// i18n keys constants
export const i18n_LOGIN: 'LOGIN'
export const i18n_EMAIL: 'EMAIL'
export const i18n_PASSWORD: 'PASSWORD'
// ...
// layout constants
export const HEADER_HEIGHT = 40
export const NAVBAR_HEIGHT = 20
export const LEFT_MENU_WIDTH = 120
// ...
This is nothing more than a list of export
statements. In a CommonJS project, use module.exports
instead.
You can then use your constants as below:
// components/LoginForm.jsx
import { useTranslation } from "react-i18next"
import { i18n_LOGIN, i18n_EMAIL, i18n_PASSWORD } from "../constants"
// ...
export function LoginForm() {
const { t } = useTranslation()
// login form component...
}
All you have to do is import the constant values you need in an import
statement, and then use them in your code.
If you do not know the constants you are going to use or find it boilerplate to import them one at a time, import the entire constants layer file with:
import * as Constants from "constants"
You can now access every constant in constants.js
as in the line below:
Constants.BASE_URL // 'https://example.com/'
Great! See the pros and cons of this approach.
π Pros:
- Easy to implement
- All your constants in the same file
π Cons:
- Not scalable
- The
constants.js
file can easily become a mess
Approach #2: Organize constants in a dedicated layer
A large project may include several hundred constants. Storing them all in a single constants.js
file is not suitable. Instead, you should break up the list of constants into several contextual files. That is what this approach to organizing JavaScript constants is all about.
Create a constants
folder in the root folder of your project. This represents the constants layer of your architecture and will contain all your constants files. Identify logical categories to organize your constants into, and create a file for each of them.
For example, you could have an api.js
file storing API-related constants as below:
// constants/api.js
export const GET = 'GET'
export const POST = 'POST'
export const PUT = 'PUT'
export const PATCH = 'PATCH'
export const DELETE = 'DELETE'
export const BASE_URL = 'https://example.com/'
// ...
And anΒ i18n.js
Β constants file containing translation constants as follows:
// constants/i18n.js
export const i18n_LOGIN: 'LOGIN'
export const i18n_EMAIL: 'EMAIL'
export const i18n_PASSWORD: 'PASSWORD'
At the end of this process, theΒ constants
Β file structure will be like this:
constants
βββ api.js
.
.
.
βββ i18n.js
.
.
.
βββ semaphore.js
To use the constants in your code, you now have to import them from the specific constants file:
import { GET, BASE_URL } from "constants/api"
The problem with this approach is that different files inside theΒ constants
Β folder might export constants with the same name. In this case, you would need to specify an alias:
import { GET, BASE_URL } from "constants/api"
import { BASE_URL as AWS_BASE_URL } from "constants/aws" // cannot import "BASE_URL" again
Unfortunately, having to define forced aliases is not great.
To address that issue, wrap your constants in a named object and then export it. For example,Β constants/api.js
Β would become:
// constants/api.js
export const APIConstants = Object.freeze({
GET: 'GET',
POST: 'POST',
PUT: 'PUT',
PATCH: 'PATCH',
DELETE: 'DELETE',
BASE_URL: 'https://example.com/',
// ...
})
export
statements are now the properties of a constants object. Note that Object.freeze()
is required to make existing properties non-writable and non-configurable. In other words, it freezes the object to the current state, making it ideal to store constants.
You can now import your constant objects with:
import { APIConstants } from "constants/api"
import { AWSConstants } from "constants/aws"
And use them in your code as in the example below:
// retrieve player data with an API call
const players = await fetch(`${APIConstants.BASE_URL}/getPlayers`)
// ...
// upload the image of a player to AWS
await fetch(`${AWSConstants.BASE_URL}/upload-image`, {
method: APIConstants.POST,
data: playerImage,
})
Adopting constants objects brings two main improvements. First, it addresses the overlapping variable name concern. Second, it makes it easier to understand what the constant refers to. After all, the APIConstants.BASE_URL
and AWSConstants.BASE_URL
statements are self-explanatory but BASE_URL
is not.
Fantastic! Let’s now explore the pros and cons of this approach.
π Pros:
- Scalable
- Improved code readability and maintainability
- Easier to find and organize constants
π Cons:
- Finding intuitive categories to organize constants into might not be that easy.
Approach #3: Export all your constants to environment variables
Another way to export all your constants to the same place is to replace them with environment variables. Take a look at the example below:
import express from 'express'
import { PlayerService } from './services/playerService'
const app = express()
// get a specific player by ID
app.get('/api/v1/players/:id', (req, res) => {
const playerId = parseInt(req.params.id)
// retrieve the player specified by the ID
const player = PlayerService.get(playerId)
// if the player was not found,
// return a 404 error with a generic message
if (player) {
return res.status(404).json({ message: process.env.ENTITY_NOT_FOUND_MESSAGE })
}
res.json(player)
})
// get all players with optional filtering options
app.get('/api/players', (req, res) => {
let filters = {
minScore: req.query.minScore,
maxScore: req.query.minScore
}
if ((minScore !== undefined && minScore < 0) || (maxScore !== undefined && maxScore < 0) || minScore > maxScore) {
return res.status(400).json({ message: process.env.INVALID_QUERY_PARAMETERS_MESSAGE })
}
const filteredPlayers = PlayerService.getAll(filters)
res.json(filteredPlayers)
})
// other endpoints...
// start the server
app.listen(3000, () => {
console.log(`Server running on port ${port}`)
})
Note that the error message strings are read from the envs through the process.env
object.
The constants will now come from .env
files or system environment variables and will no longer be hard-coded in the code. That is great for security but also comes with a couple of pitfalls. The main drawback is that you are just replacing hard-coded constant values with process.env
instructions. As the same env might be in use in different part of your codebase, you are still subject to same code duplication issues presented initially. Also, the application may crash or produce unexpected behavior when a required environment variable is not defined properly.
However, the idea behind this implementation should not be discarded altogether. To get the most out of it, you need to integrate it with the approaches presented earlier. Consider the BASE_URL
constant, for example. That is likely to change based on the environment you are working on. Thus, you do not want to hard-code it in your code and can replace it with an environment variable.
With theΒ constants.js
Β approach, you would get:
// constants.js
// API constants
export const GET = 'GET'
export const POST = 'POST'
export const PUT = 'PUT'
export const PATCH = 'PATCH'
export const DELETE = 'DELETE'
export const BACKEND_BASE_URL = process.env.BACKEND_BASE_URL || 'https://example.com'
// ...
WhileΒ APIConstants.js
Β would become:
// constants/api.js
export const APIConstants = Object.freeze({
GET: 'GET',
POST: 'POST',
PUT: 'PUT',
PATCH: 'PATCH',
DELETE: 'DELETE',
BASE_URL: process.env.BACKEND_BASE_URL || 'https://example.com',
// ...
})
Note that only some constants are read from environment variables. In detail, all constants that depend on the deployment environment or contain secrets should be exported to the envs. This mechanism also allows you to specify default values for when an env is missing.
π Pros:
- Values change based on the deployment environment
- Prevent secrets to be publicly exposed in the code
- Complementary to the other approaches
π Cons:
- Code duplication and maintainability issues when used alone
Congrats! You just saw different ways to build an effective and elegant constants layer in JavaScript.
Structuring Constants in JavaScript: Approach Comparison
Explore the differences between the three approaches to structure constants in JavaScript:
Aspect | Single file approach | Multiple file approach | Environment variable approach |
---|---|---|---|
Description | Store all constants in a single file | Organize constants into dedicated files | Read constants from environment variables |
Number of Files | 1 (constants.js ) | Multiple files (one per logical category of constants, such as APIConstants.js , TranslationConstants.js , etc.) | – 0 (when using system environment variables) – 1 or more (when relying on .env files) |
Implementation difficulty | Easy to implement | Easy to implement but hard to define the right categories | Easy to implement |
Scalability | Limited | Highly scalable | Limited |
Maintainability | Great | Great | Limited if not integrated with the other two approaches |
Constants organization | Becomes messy with a large number of constants | Always highly organized | Becomes messy with a large number of constants |
Use cases | Small projects with few constants | Medium to large projects with dozens of constants | To protect secret and deployment environment-dependent constants. |
Conclusion
In this article, you learned what a constants layer is, why you need it in your JavaScript project, how to implement it, and what benefits it can bring to your architecture. A constants layer is a portion of your architecture that contains all your constants. It centralizes the constants management and consequently allows you to avoid code duplication. Implementing it in JavaScript is not complex, and here you saw three different approaches to do so.