13 Dec 2023 · Software Engineering

    How To Organize Constants in a Dedicated Layer in JavaScript

    13 min read
    Contents

    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_URLstatements 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:

    AspectSingle file approachMultiple file approachEnvironment variable approach
    DescriptionStore all constants in a single fileOrganize constants into dedicated filesRead constants from environment variables
    Number of Files1 (constants.js)Multiple files (one per logical category of constants, such as APIConstants.jsTranslationConstants.js, etc.)– 0 (when using system environment variables) 
    – 1 or more (when relying on .env files)
    Implementation difficultyEasy to implementEasy to implement but hard to define the right categoriesEasy to implement
    ScalabilityLimitedHighly scalableLimited
    MaintainabilityGreatGreatLimited if not integrated with the other two approaches
    Constants organizationBecomes messy with a large number of constantsAlways highly organizedBecomes messy with a large number of constants
    Use casesSmall projects with few constantsMedium to large projects with dozens of constantsTo 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.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    I'm a software engineer, but I prefer to call myself a Technology Bishop. Spreading knowledge through writing is my mission.
    Avatar
    Reviewed by:
    I picked up most of my skills during the years I worked at IBM. Was a DBA, developer, and cloud engineer for a time. After that, I went into freelancing, where I found the passion for writing. Now, I'm a full-time writer at Semaphore.