Contact Me

12 Factors

by Heroku

 The 12-Factor App methodology is a set of best practices for building scalable and maintainable web applications. It is designed to help developers build applications that are easy to deploy, scale, and manage. The methodology consists of 12 factors that should be considered when building an application.



I. Codebase

 - One codebase tracked in revision control, many deploys.


 Currently the most common way to do this is with git. One of the methodologies used with git is git flow and semantic commits

 Git flow is a branching model for Git, a popular version control system. It is a set of conventions for branching and merging that are designed to help developers collaborate on code while keeping the main branch (typically called "master") stable. The basic idea behind Git flow is to use different branches for different stages of development, such as feature branches for new features, release branches for preparing for a release, and hotfix branches for quick fixes to production code. As shown in the following image

gitFlowImg

 Semantic commits are a way of structuring commit messages in a consistent and meaningful way. The idea is to use a specific format for commit messages that makes it easy to understand the purpose of a commit at a glance. A common format for semantic commits is to start the message with a verb in the present tense, followed by a brief description of the change. For example, "fix: fix bug in login page" or "add: new feature xyz"

For example:

// Start by creating a new feature branch off the develop branch using the command:

git checkout -b feature/new-feature

// Make changes to the code and add them to the staging area using:

git add .

// Commit the changes with a semantic commit message using:

git commit -m "add: new feature xyz"

// When the feature is complete and ready to be merged, open a pull request (PR) to the develop branch.

// Once the PR is reviewed and approved, merge the feature branch into develop using a semantic merge commit message, for example:

git checkout develop

git merge --no-ff feature/new-feature -m "merge: feature xyz"


II. Dependencies

Explicitly declare and isolate dependencies.


 This principle states that an application should explicitly declare all of its dependencies and should not rely on any implicit dependencies or system-wide packages. This makes it easier to understand and manage the dependencies of the application, and also makes it easier to run the same codebase in different environments.

 In JavaScript, dependencies are typically managed using a package manager such as npm or yarn. Here's an example of how you might declare dependencies in a JavaScript application using npm:

For example:

npm init -y

npm i express axios dotenv

// package.json

{

 "dependencies": {

  "express": "^4.17.1",

  "axios": "^0.21.0",

  "dotenv": "^8.2.0"

}

}

 In this example, the application has declared three dependencies: express, axios, and dotenv. These packages can be installed by running the command npm install in the project's root directory.


III. Config

 - Store config in the environment.


 This principle states that an application should store its configuration in environment variables rather than hard-coding it into the codebase. This allows for easy modification of the configuration without having to change and redeploy the code, and also makes it easier to run the same codebase in different environments (e.g. development, staging, production) with different configurations.

 Currently the most used standard is to have configuration variables with capital letters

For example:

const PORT = process.env.PORT || 3000;

 In the case of Node.js, a framework called dotenv can be used to automate the import of these variables

require('dotenv').config()

const dbUrl = process.env.DATABASE_URL;

 This way, you can have a .env file in your root directory with your environment variables and the library will load them into the process.env object, so you can access them in your code.


IV. Backing services

 - Treat backing services as attached resources.


 The most important part is that backing services are treat as attached resources, they can be attached and detached from the app as needed, allowing the app to scale horizontally. Examples of backing services include databases, message queues, and email services.

 For example, you could create an adapter for each type of backing service that your application uses

class DbAdapter {

constructor(options) {

  this.connection = mysql.createConnection(options);

}

query(sql, params) {

  return new Promise((resolve, reject) => {

   this.connection.query(sql, params, (err, results) => {

    if (err) {

     reject(err);

    } else {

     resolve(results);

    }

   });

  });

}

}

const db = new DbAdapter({

host: 'localhost',

user: 'root',

password: 'password',

database: 'mydb'

});

 This makes it easy to switch out the database implementation if needed.


V. Build, release, run

 - Strictly separate build and run stages.


 It refers to the process of creating, packaging, and deploying an application. This factor emphasizes the importance of separating the build and release processes from the running of the application, in order to make it easier to manage and update the application.

 For example, you can then use a package manager such as npm or yarn to manage the dependencies of your application, and scripts to automate the build, release, and run processes.

// package.json

{

 "scripts": {

 "build": "webpack --config webpack.config.js",

 "start": "node dist/bundle.js"

},

"dependencies": {

 "babel-loader": "^8.2.2",

 "file-loader": "^6.1.1"

}

}

 By separating the build and release processes from the running of the application, you can more easily manage and update the application without having to worry about affecting the running application. This can help you to deploy new versions of your application more quickly and with less risk.


VI. Processes

 - Execute the app as one or more stateless processes.


 The application should not rely on any state that is stored in memory or on disk between requests, and should instead rely on backing services for any state that needs to be persisted, typically a database.

 For example, you can then use PrismaORM. It's a powerful tool that can help you to follow the Processes factor , by abstracting away the low-level details of interacting with the database, and allowing you to work with your data in a more intuitive way with type-safety.

Here is the code of a prisma schema file:

generator client {

  provider = "prisma-client-js"

}

datasource db {

  provider = "postgresql"

  url = env("DATABASE_URL")

}

model User {

  id String @id @default(uuid())

  name String

  age Int

  email String @unique

}


VII. Port binding

 - Export services via port binding.


 This means that an application should not rely on runtime injection of a web server into the execution environment to create a web-facing service. Instead, the web server should be a part of the application itself, and the application should bind to the appropriate port to listen for incoming requests.

 In modern applications, this is already the default way make an application, but it is also common to allow the entry of an environment variable for flexibility reasons.

For example:

const express = require('express');

const app = express();

app.listen(PORT || 3000, () => console.log('Server running on port 3000'));


VIII. Concurrency

 - Scale out via the process model.


 This means that the application should be designed to handle multiple requests at the same time, and should be able to run multiple instances of the same process type in parallel.

 In JavaScript, this can be achieved by using the Node.js's built-in support for concurrency to handle multiple requests at the same time.

const express = require('express');

const cluster = require('cluster');

const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {

for (let i = 0; i < numCPUs ; i++) {

   cluster.fork();

}

  cluster.on('exit', (worker, code, signal) => {}

} else {

   const app = express();

   app.get('/', (req, res) => {

    res.send('Hello, World!');

   });

  app.listen(3000, () => console.log(`Worker $198 is listening on port 3000`));

}

 But in case your system uses a container orchestrator, for example Kubernetes, it is recommended you let is handles the concurrency problems.


IX. Disposability

 - Maximize robustness with fast startup and graceful shutdown.


 This means that an application should be able to handle being started, stopped, and restarted quickly and without any significant loss of data or state.

 For example, in JavaScript, you can use process managers like PM2, to handle the starting and stopping of the application processes and to automatically restart the processes if they fail.

const pm2 = require('pm2');

pm2.connect(() => {

 pm2.start({

  name: 'app',

  script: 'app.js',

  instances: 2,

  watch: true,

  max_memory_restart: '100M'

  }, (err) => {

   if (err) {

   console.error(err);

   process.exit(2);

   }

});

});


X. Dev/prod parity

 - Keep development, staging, and production as similar as possible.


 It states that the development, staging, and production environments should be as similar as possible, in order to minimize the differences between environments and reduce the potential for errors or bugs.

 You can use a tool like dotenv, which allows you to define environment variables in a .env file and load them into your application at runtime. This way, you can use the same configuration settings across all environments.

For example:

require('dotenv').config();

console.log(process.env.DATABASE_URL);


XI. Logs

 - Treat logs as event streams.


 It states that the application should not attempt to write to or manage logfiles directly, but instead should write all log events to stdout and stderr. The reasoning behind this is that logs should be treated as event streams, which can be aggregated, monitored, and analyzed separately from the application itself.

 In JavaScript, one way to implement this is to use a logging library that writes all log events to the console. For example winston:

const winston = require('winston');

const logger = winston.createLogger({

 level: 'info',

 format: winston.format.json(),

 transports: [

  new winston.transports.Console()

 ]

});

logger.info('Application started at:', new Date());

 The winston library is used to create a logger object, which is configured to write log events to the console at the info level.


XII. Admin processes

 - Run admin/management tasks as one-off processes.


 It states that the application should treat runtime admin and management tasks as a first-class citizen, and should have a separate process for running administrative commands and tasks.

 For example, in a Node.js application, you can use gulp to define tasks that can be run from the command line, such as cleaning up the build directory, linting the code, or running test suites.

const gulp = require('gulp');

const eslint = require("gulp-eslint");

gulp.task("lint", function() {

return gulp.src(['src/**/*.js'])

  .pipe(eslint())

  .pipe(eslint.format())

  .pipe(eslint.failAfterError());

});

 The winston library is used to create a logger object, which is configured to write log events to the console at the info level.