DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Building a Full-Stack Resume Screening Application With AI
  • A Practical Guide to Securing NodeJS APIs With JWT
  • Node.js Walkthrough: Build a Simple Event-Driven Application With Kafka
  • Node.js: Architectural Gems and Best Practices for Developers to Excel

Trending

  • Understanding Java Signals
  • Solid Testing Strategies for Salesforce Releases
  • Ensuring Configuration Consistency Across Global Data Centers
  • Grafana Loki Fundamentals and Architecture
  1. DZone
  2. Coding
  3. JavaScript
  4. Initializing Services in Node.js Application

Initializing Services in Node.js Application

An in-depth guide on managing service initialization in Node.js applications, illustrated with a refined JWT Service example.

By 
Anton Kalik user avatar
Anton Kalik
·
Mar. 18, 24 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
5.5K Views

Join the DZone community and get the full member experience.

Join For Free

While working on a user model, I found myself navigating through best practices and diverse strategies for managing a token service, transitioning from straightforward functions to a fully-fledged, independent service equipped with handy methods. I delved into the nuances of securely storing and accessing secret tokens, discerning between what should remain private and what could be public. Additionally, I explored optimal scenarios for deploying the service or function and pondered the necessity of its existence. This article chronicles my journey, illustrating the evolution from basic implementations to a comprehensive, scalable solution through a variety of examples.

Services

In a Node.js application, services are modular, reusable components responsible for handling specific business logic or functionality, such as user authentication, data access, or third-party API integration. These services abstract away complex operations behind simple interfaces, allowing different parts of the application to interact with these functionalities without knowing the underlying details. By organizing code into services, developers achieve separation of concerns, making the application more scalable, maintainable, and easier to test. Services play a crucial role in structuring the application’s architecture, facilitating a clean separation between the application’s core logic and its interactions with databases, external services, and other application layers. I decided to show an example with JWT Service. Let’s jump to the code.

First Implementation

In our examples, we are going to use jsonwebtoken as a popular library in the Node.js ecosystem. It will allow us to encode, decode, and verify JWTs easily. This library excels in situations requiring the safe and quick sharing of data between web application users, especially for login and access control.

To create a token:

TypeScript
 
jsonwebtoken.sign(payload, JWT_SECRET)


and verify:

TypeScript
 
jsonwebtoken.verify(token, JWT_SECRET, (error, decoded) => {
  if (error) {
    throw error
  }

  return decoded;
});


For the creation and verifying tokens we have to have JWT_SECRET which lying in env.

TypeScript
 
process.env.JWT_SECRET


That means we have to read it to be able to proceed to methods.

TypeScript
 
if (!JWT_SECRET) {
  throw new Error('JWT secret not found in environment variables!');
}


So, let’s sum it up to the one object with methods:

TypeScript
 
require('dotenv').config();
import jsonwebtoken from 'jsonwebtoken';

const JWT_SECRET = process.env.JWT_SECRET!;

export const jwt = {
  verify: <Result>(token: string): Promise<Result> => {
    if (!JWT_SECRET) {
      throw new Error('JWT secret not found in environment variables!');
    }

    return new Promise((resolve, reject) => {
      jsonwebtoken.verify(token, JWT_SECRET, (error, decoded) => {
        if (error) {
          reject(error);
        } else {
          resolve(decoded as Result);
        }
      });
    });
  },
  sign: (payload: string | object | Buffer): Promise<string> => {
    if (!JWT_SECRET) {
      throw new Error('JWT secret not found in environment variables!');
    }

    return new Promise((resolve, reject) => {
      try {
        resolve(jsonwebtoken.sign(payload, JWT_SECRET));
      } catch (error) {
        reject(error);
      }
    });
  },
};


jwt.ts file jwt Object With Methods

This object demonstrates setting up JWT authentication functionality in a Node.js application. To read env variables helps: require(‘dotenv’).config();and with access to process, we are able to get JWT_SECRET value. Let’s reduce repentance of checking the secret.

TypeScript
 
checkEnv: () => {
  if (!JWT_SECRET) {
    throw new Error('JWT_SECRET not found in environment variables!');
  }
},


Incorporating a dedicated function within the object to check the environment variable for the JWT secret can indeed make the design more modular and maintainable. But still some repentance, because we still have to call it in each method: this.checkEnv();

Additionally, I have to consider the usage of this context because I have arrow functions. My methods have to become function declarations instead of arrow functions for verify and sign methods to ensure this.checkEnvworks as intended.

Having this we can create tokens:

TypeScript
 
const token: string = await jwt.sign({
  id: user.id,
})


or verify them:

TypeScript
 
jwt.verify(token)


At this moment we can think, is not better to create a service that is going to handle all of this stuff?

Token Service

By using the service we can improve scalability. I still checking the existing secret within the TokenService for dynamic reloading of environment variables (just as an example), I streamline it by creating a private method dedicated to this check. This reduces repetition and centralizes the logic for handling missing configurations:

TypeScript
 
require('dotenv').config();
import jsonwebtoken from 'jsonwebtoken';

export class TokenService {
  private static jwt_secret = process.env.JWT_SECRET!;

  private static checkSecret() {
    if (!TokenService.jwt_secret) {
      throw new Error('JWT token not found in environment variables!');
    }
  }

  public static verify = <Result>(token: string): Promise<Result> => {
    TokenService.checkSecret();

    return new Promise((resolve, reject) => {
      jsonwebtoken.verify(token, TokenService.jwt_secret, (error, decoded) => {
        if (error) {
          reject(error);
        } else {
          resolve(decoded as Result);
        }
      });
    });
  };

  public static sign = (payload: string | object | Buffer): Promise<string> => {
    TokenService.checkSecret();

    return new Promise((resolve, reject) => {
      try {
        resolve(jsonwebtoken.sign(payload, TokenService.jwt_secret));
      } catch (error) {
        reject(error);
      }
    });
  };
}


TokenService.ts File

But I have to consider moving the check for the presence of necessary configuration outside of the methods and into the initialization or loading phase of my application, right? This ensures that my application configuration is valid before it starts up, avoiding runtime errors due to missing configuration. And in this moment the word proxy comes to my mind. Who knows why, but I decided to check it out:

Service With Proxy

First, I need to refactor my TokenService to remove the repetitive checks from each method, assuming that the secret is always present:

TypeScript
 
require('dotenv').config();
import jsonwebtoken from 'jsonwebtoken';

export class TokenService {
  private static jwt_secret = process.env.JWT_SECRET!;

  public static verify<TokenPayload>(token: string): Promise<TokenPayload> {
    return new Promise((resolve, reject) => {
      jsonwebtoken.verify(token, TokenService.jwt_secret, (error, decoded) => {
        if (error) {
          reject(error);
        } else {
          resolve(decoded as TokenPayload);
        }
      });
    });
  }

  public static sign(payload: string | object | Buffer): Promise<string> {
    return new Promise((resolve, reject) => {
      try {
        resolve(jsonwebtoken.sign(payload, TokenService.jwt_secret));
      } catch (error) {
        reject(error);
      }
    });
  }
}


Token Service Without Checking Function the Secret

Then I created a proxy handler that checks the JWT secret before forwarding calls to the actual service methods:

TypeScript
 
const tokenServiceHandler = {
  get(target, propKey, receiver) {
    const originalMethod = target[propKey];

    if (typeof originalMethod === 'function') {
      return function(...args) {
        if (!TokenService.jwt_secret) {
          throw new Error('Secret not found in environment variables!');
        }

        return originalMethod.apply(this, args);
      };
    }

    return originalMethod;
  }
};


Token Service Handler

Looks fancy. Finally, for the usage of the proxied token service, I have to create an instance of the Proxy class:

TypeScript
 
const proxiedTokenService = new Proxy(TokenService, tokenServiceHandler);


Now, instead of calling TokenService.verify or TokenService.sign directly, I can use proxiedTokenService for these operations. The proxy ensures that JWT secret check is performed automatically before any method logic is executed:

TypeScript
 
try {
  const token = proxiedTokenService.sign({ id: 123 });
  console.log(token);
} catch (error) {
  console.error(error.message);
}

try {
  const payload = proxiedTokenService.verify('<token>');
  console.log(payload);
} catch (error) {
  console.error(error.message);
}


This approach abstracts away the repetitive pre-execution checks into the proxy mechanism, keeping this method's implementations clean and focused on their core logic. The proxy handler acts as a middleware layer for my static methods, applying the necessary preconditions transparently.

Constructor

What about constructor usage? There’s a significant distinction between initializing and checking environment variables in each method call; the former approach doesn’t account for changes to environment variables after initial setup:

TypeScript
 
export class TokenService {
  private jwt_secret: string;

  constructor() {
    if (!process.env.JWT_SECRET) {
      throw new Error('JWT secret not found in environment variables!');
    }
    this.jwt_secret = process.env.JWT_SECRET;
  }

  public verify(token: string) {
    // Implementation...
  }

  public sign(payload) {
    // Implementation...
  }
}

const tokenService = new TokenService();


Constructor Approach

The way the service is utilized will stay consistent; the only change lies in the timing of the service’s initialization.

Service Initialization

We’ve reached the stage of initialization where we can perform necessary checks before using the service. This is a beneficial practice with extensive scalability options.

TypeScript
 
require('dotenv').config();
import jsonwebtoken from 'jsonwebtoken';

export class TokenService {
  private static jwt_secret: string = process.env.JWT_SECRET!;

  static initialize = () => {
    if (!this.jwt_secret) {
      throw new Error('JWT secret not found in environment variables!');
    }

    this.jwt_secret = process.env.JWT_SECRET!;
  };

  public static verify = <Result>(token: string): Promise<Result> =>
    new Promise((resolve, reject) => {
      jsonwebtoken.verify(token, TokenService.jwt_secret, (error, decoded) => {
        if (error) {
          reject(error);
        } else {
          resolve(decoded as Result);
        }
      });
    });

  public static sign = (payload: string | object | Buffer): Promise<string> =>
    new Promise((resolve, reject) => {
      try {
        resolve(jsonwebtoken.sign(payload, TokenService.jwt_secret));
      } catch (error) {
        reject(error);
      }
    });
}


Token Service With Initialization

Initialization acts as a crucial dependency, without which the service cannot function. To use this approach effectively, I need to call TokenService.initialize() early in my application startup sequence, before any other parts of my application attempt to use the TokenService. This ensures that my service is properly configured and ready to use.

TypeScript
 
import { TokenService } from 'src/services/TokenService';

TokenService.initialize();


This approach assumes that my environment variables and any other required setup do not change while my application is running. But what if my application needs to support dynamic reconfiguration, I might consider additional mechanisms to refresh or update the service configuration without restarting the application, right?

Dynamic Reconfiguration

Supporting dynamic reconfiguration in the application, especially for critical components like TokenService that rely on configurations like JWT_SECRET, requires a strategy that allows the service to update its configurations at runtime without a restart.

For that, we need something like configuration management which allows us to refresh configurations dynamically from a centralized place. Dynamic configuration refresh mechanism — this could be a method in my service that can be called to reload its configuration without restarting the application:

TypeScript
 
export class TokenService {
  private static jwt_secret = process.env.JWT_SECRET!;

  public static refreshConfig = () => {
    this.jwt_secret = process.env.JWT_SECRET!;
    if (!this.jwt_secret) {
      throw new Error('JWT secret not found in environment variables!');
    }
  };

  // our verify and sign methods will be the same
}


Token Service With Refreshing Config

I need to implement a way to monitor my configuration sources for changes. This could be as simple as watching a file for changes or as complex as subscribing to events from a configuration service. This is just an example:

TypeScript
 
import fs from 'fs';

fs.watch('config.json', (eventType, filename) => {
  if (filename) {
    console.log(`Configuration file changed, reloading configurations.`);
    TokenService.refreshConfig();
  }
});


If active monitoring is not feasible or reliable, we can consider scheduling periodic checks to refresh configurations. This approach is less responsive but can be sufficient depending on how frequently my configurations change.

Cron Job

Another example can be valuable with using a cron job within a Node.js application to periodically check and refresh configuration for services, such as a TokenService, is a practical approach for ensuring my application adapts to configuration changes without needing a restart. This can be especially useful for environments where configurations might change dynamically (e.g., in cloud environments or when using external configuration management services).

For that, we can use node-cron package to achieve the periodical check:

TypeScript
 
import cron from 'node-cron''
import { TokenService } from 'src/services/TokenService'

cron.schedule('0 * * * *', () => {
  TokenService.refreshConfiguration();
}, {
  scheduled: true,
  timezone: "America/New_York"
});

console.log('Cron job scheduled to refresh TokenService configuration every hour.');


Cron Job periodically checks the latest configurations.

In this setup, cron.schedule is used to define a task that calls TokenService.refreshConfiguration every hour ('0 * * * *' is a cron expression that means "at minute 0 of every hour").

Conclusion

Proper initialization ensures the service is configured with essential environment variables, like the JWT secret, safeguarding against runtime errors and security vulnerabilities. By employing best practices for dynamic configuration, such as periodic checks or on-demand reloading, applications can adapt to changes without downtime. Effectively integrating and managing the TokenService enhances the application's security, maintainability, and flexibility in handling user authentication.

I trust this exploration has provided you with meaningful insights and enriched your understanding of service configurations.

application JWT (JSON Web Token) Node.js

Published at DZone with permission of Anton Kalik. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Building a Full-Stack Resume Screening Application With AI
  • A Practical Guide to Securing NodeJS APIs With JWT
  • Node.js Walkthrough: Build a Simple Event-Driven Application With Kafka
  • Node.js: Architectural Gems and Best Practices for Developers to Excel

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!