Skip to navigation

NodeJS Code Structure

This project is a Fastify-based application that integrates with Azure Service Bus for message processing. It follows a modular and scalable structure to ensure maintainability.

Project Structure

πŸ“‚ node-project
β”œβ”€β”€ πŸ“‚ .docker # Dockerfiles and docker-compose.yaml
β”œβ”€β”€ πŸ“‚ database # Temporary location for our database schema scripts
β”œβ”€β”€ πŸ“‚ dist # TypeScript compiler results (not committed)
β”œβ”€β”€ πŸ“‚ reports # Code quality reports (currently only coverage reports)
β”œβ”€β”€ πŸ“‚ src # All of the implementation source code
β”‚ β”œβ”€β”€ πŸ“‚ config # Configuration files (e.g., Azure Service Bus host settings)
β”‚ β”œβ”€β”€ πŸ“‚ controllers # API request/response handlers and schema
β”‚ β”œβ”€β”€ πŸ“‚ internals # Reusable code that could become a new node package
β”‚ β”œβ”€β”€ πŸ“‚ routes # API route definitions
β”‚ β”œβ”€β”€ πŸ“‚ services # Business logic (upload, copy file, send to extractor, etc…)
β”‚ β”œβ”€β”€ πŸ“‚ subscribers # Azure Service Bus message subscribers
β”‚ β”œβ”€β”€ πŸ“‚ types # Type definitions that get re-used in different parts of the pp
β”‚ β”œβ”€β”€ πŸ“‚ utils # Junk drawer (Ideally empty)
β”‚ └── πŸ“„ index.ts # Initializes and starts the application
β”œβ”€β”€ πŸ“‚ test # ALL automated tests
β”‚ └── πŸ“‚ unit # ALL Unit tests (ONLY Unit tests)
β”œβ”€β”€ πŸ“„ .env # Environment variables FOR CONFIGURATION (not committed)
β”œβ”€β”€ πŸ“„ .npmrc # Configuration that allows us to use our private npm registry
β”œβ”€β”€ πŸ“„ .gitignore # Files and folders for git to ignore
β”œβ”€β”€ πŸ“„ eslint.config.ts # ESLint rules configuration
β”œβ”€β”€ πŸ“„ package.json # Dependencies and scripts
β”œβ”€β”€ πŸ“„ package-lock.json # Used by npm to fetch consistent dependencies (not committed)
β”œβ”€β”€ πŸ“„ pull_request_template.md # Pull request template with the checklist in it
β”œβ”€β”€ πŸ“„ README.md # Information about this project
β”œβ”€β”€ πŸ“„ tsconfig.json # TypeScript compiler configuration
└── πŸ“„ vitest.config.ts # Test configuration for vitest framework


πŸ“‚ config - Configuration

Stores configuration files such as Azure Service Bus credentials.

Not TypeScript files unless that is the config file format that is required.

πŸ“„ config/appConfig.json

{
"port": 3000,
"azureServiceBusConnectionString": 'SomeActualServiceBusConnectionString',
};

πŸ“‚ src - All of the TypeScript implementation files for our microservice

πŸ“‚ src/controllers - API request/response handlers and schema

πŸ“„ src/controllers/message.ts

import { azureServiceBusService } from '../../services/azureServiceBusService.js';
export const messageController = {
send: async (request: FastifyRequest, reply: FastifyReply) => {
try {
const message = request.body?.message;
await azureServiceBusService.sendMessageToQueue('message-queue', message);
return reply.send({ status: 'Message sent' });
} catch (error) {
request.log.error(error);
await reply.status(500).send(formatErrorAsProblemJson(error));
throw error;
}
}
};
export const messageSchema = {
body: {
type: 'object',
properties: {
name: { type: 'string' },
excitement: { type: 'integer' }
}
},
response: {
200: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}

πŸ“‚ src/internals - Reusable code that could become a new node package

As we discover functionality that we want to reuse we can create a subfolder here to hold all of the related functionality. For example, if we begin by subscribing to a single Service Bus topic but later realize we need to subscribe to more than one we could create a subfolder here called something like β€œserviceBusSubscribe” and we could put a general purpose function that can take a topic name and a function to handle each message so that when we need to create a subscriber for a particular topic we can do it the same way each time. This repetition might be within the current project OR across multiple microservices.

There could be other reasons to create a separate node package for some of our code. One example is that we have created a feature that takes a long time to run the test suite for and which we change very seldom. If we put that code and the related tests into a separate package we can pay the price of running those tests only when we need to change that feature. So if that test suite took 2 minutes to run, instead of waiting 2 minutes in our microservice for each test run, we can save 2 minutes for each test run in the microservice and only need that 2 minute test suite when we are changing that package.


πŸ“‚ src/routes - All route assignments for our REST API routes

πŸ“„ src/routes/customeRoute.ts

import { FastifyInstance } from 'fastify';
import { messageController, messageSchema } from '../controllers/message.js';
export const customeRoute = async (fastify: FastifyInstance) {
fastify.post('/send', { schema: messageSchema },
async (request: FastifyRequest, reply: FastifyReply) => {
await messageController.send(request, reply);
});
}

πŸ“‚ src/plugins - Fastify Plugins

(PLEASE DO NOT USE THIS FOLDER UNTIL WE TALK ABOUT IT)

Handles database connections and Azure Service Bus setup.

πŸ“„ src/plugins/azureServiceBus.ts

import { ServiceBusClient } from '@azure/service-bus';
import { config } from '../config/index.js';
let client: ServiceBusClient;
export const azureServiceBusPlugin = async function (fastify: any) {
try {
client = new ServiceBusClient(config.azureServiceBusConnectionString);
fastify.decorate('azureServiceBus', client);
fastify.log.info('Azure Service Bus Connected');
} catch (err) {
fastify.log.error('Azure Service Bus Connection Error:', err);
process.exit(1);
}
};
export { client };

πŸ“‚ src/services - Business Logic

Contains logic to send messages to Azure Service Bus.

πŸ“„ src/services/azureServiceBusService.ts

import { ServiceBusClient } from '@azure/service-bus';
import { config } from '../config/index.js';
const client = new ServiceBusClient(config.azureServiceBusConnectionString);
const sender = client.createSender('message-queue');
export const enqueue = async (queueName: string, message: object) => {
try {
await sender.sendMessages({ body: JSON.stringify(message) });
console.log(`Message sent to Azure Service Bus queue: ${queueName}`);
} catch (error) {
console.error('Error sending message to Azure Service Bus:', error);
}
};

πŸ“‚ src/subscribers - Message Consumers

Consumes messages from Azure Service Bus and processes them. If we find a source of messages other than the Azure Service Bus, it seems like handlers for those messages belong here too.

πŸ“„ src/subscribers/messageQueueSubscriber.ts

import { ServiceBusClient } from '@azure/service-bus';
import config from '../config/index.js';
const client = new ServiceBusClient(config.azureServiceBusConnectionString);
const receiver = client.createReceiver('message-queue');
const startConsumer = async () => {
receiver.subscribe({
processMessage: async (message) => {
console.log('πŸ“© Received Message:', message.body);
},
processError: async (error) => {
console.error('Error processing message:', error);
},
});
console.log('Azure Service Bus Consumer started...');
};
// This is an older example. Please do not do this in real code
startConsumer().catch(console.error);

πŸ“„ src/index.ts - Framework Initialization and Entry Point

Initializes DevFramework, registers plugins, enables features, and starts the server.

import { DevFramework } from "framework-nodejs-core";
import 'framework-nodejs-appconfig';
// Initialize the framework
const app = await DevFramework.create();
// Enable middleware
await app.enableMiddleware();
// Initialize the appConfig
await app.initAppConfig({
azureAppConfigOptions: {
useManagedIdentity: true,
endpoint: process.env.FRAMEWORK_AZURE_APP_CONFIG_ENDPOINT,
},
useEnvironmentVariables: true,
});
// Start the server
app.start(3000);

πŸ“‚ test/unit - ALL Unit tests (ONLY Unit tests)

The folder structure in the test/unit folder should mirror the folder structure in the src folder. In other words, if there is a src/services/awesomeService.ts then the unit tests would be in test/unit/services/awesomeService.test.ts.


πŸ“„ .env - Environment Variables

(PLEASE HELP MAKE SURE THIS DOES NOT GET COMMITED TO THE REPO)

PORT=3000
AZURE_SERVICE_BUS_CONNECTION_STRING=your-azure-service-bus-connection-string

πŸ“„ package.json - NodeJS Package Configuration & Dependencies

This example file assumes we are building a service with a REST API using the CLA DevFramework libraries using vitest as a test-framework.

{
Β Β "name":Β "node-project",
Β Β "version":Β "1.0.0",
Β Β "main":Β "dist/index.js",
Β Β "type":Β "module",
Β Β "scripts":Β {
Β Β Β Β "build":Β "tsc",
Β Β Β Β "clean":Β "npxΒ rimrafΒ distΒ reports",
Β Β Β Β "lint":Β "eslintΒ .",
Β Β Β Β "lint-fix":Β "eslintΒ .Β --fix",
Β Β Β Β "prebuild":Β "npxΒ rimrafΒ dist",
Β Β Β Β "precoverage":Β "npxΒ rimrafΒ reports",
Β Β Β Β "start":Β "tscΒ &&Β nodeΒ .",
Β Β Β Β "test":Β "vitestΒ run",
Β Β Β Β "test-watch":Β "vitestΒ --watch",
Β Β Β Β "coverage":Β "vitestΒ runΒ --coverage"
Β Β },
Β Β "keywords":Β [],
Β Β "author":Β "",
Β Β "license":Β "ISC",
Β Β "description":Β "",
Β Β "devDependencies":Β {
Β Β Β Β "@eslint/js":Β "^9.20.0",
Β Β Β Β "@stylistic/eslint-plugin-js":Β "^4.0.1",
Β Β Β Β "@types/node":Β "22.13.16",
Β Β Β Β "@types/npmcli__package-json":Β "^4.0.4",
Β Β Β Β "@vitest/coverage-v8":Β "^3.0.7",
Β Β Β Β "eslint":Β "^9.20.1",
Β Β Β Β "eslint-plugin-import":Β "^2.31.0",
Β Β Β Β "eslint-plugin-n":Β "^17.15.1",
Β Β Β Β "eslint-plugin-unused-imports":Β "^4.1.4",
Β Β Β Β "jiti":Β "^2.4.2",
Β Β Β Β "rimraf":Β "^6.0.1",
Β Β Β Β "ts-node":Β "^10.9.2",
Β Β Β Β "typescript":Β "^5.8.2",
Β Β Β Β "typescript-eslint":Β "^8.29.0",
Β Β Β Β "vitest":Β "^3.0.9",
Β Β Β Β "vitest-mock-extended":Β "^3.1.0"
Β Β },
Β Β "dependencies":Β {
Β Β Β Β "@fastify/awilix":Β "^7.0.0",
Β Β Β Β "@fastify/multipart":Β "^9.0.3",
Β Β Β Β "@npmcli/package-json":Β "^6.1.1",
Β Β Β Β "bson-objectid":Β "^2.0.4",
Β Β Β Β "dotenv":Β "^16.4.7",
Β Β Β Β "framework-nodejs-appconfig":Β "^1.0.11",
Β Β Β Β "framework-nodejs-core":Β "^1.0.23",
Β Β Β Β "framework-nodejs-extensions":Β "^1.0.2",
Β Β Β Β "framework-nodejs-logging":Β "^1.0.8",
Β Β Β Β "framework-nodejs-web":Β "^1.0.10",
Β Β Β Β "pino-pretty":Β "^13.0.0"
Β Β }
}