Cooking recipes for web applications with Node.js, Express.js and TypeScript – Part 2

No Comments

This article and the code examples build on the first part of the article series. Have you already read it?
This article, the second part, deals with recipes that are interesting during the operation and further development of a Node.js and Express.js web application. Libraries are presented that help to build a secure application. In addition, simple rate limiting is implemented to prevent request spamming and protect critical interfaces. Advanced rate limiting in distributed systems will also be discussed.
The use of feature toggles offers many advantages during the delivery and development of software, therefore this recipe is also presented. For dessert, a recipe for the professional configuration of web applications is on the menu, as secure and flexible handling of secrets and other variables is crucial.

You already know the repository in which the following recipes are demonstrated and become a boilerplate project. As soon as all tools and ingredients from the first article are ready, you can start with the first recipe.

Web security in Express.js applications

IT security is still not taken seriously enough in software development and should be a first-class citizen in web applications. In today’s fast-moving IT it is not unusual that regular Threat Modeling sessions, penetration tests and dependency checks are neglected. There is often a lack of basic awareness of the importance of secure business logic and its technical implementation.

In order to avoid some major security flaws, the Node library Helmet helps. It contains 14 middlewares to set web security headers. They help prevent basic security problems of a web application (see OWASP Top Ten Project). Helmet’s default export is a middleware that combines the seven most common middlewares from the collection. In addition, you should check whether the use of the other middlewares is useful for your own application. This is what it looks like if, in addition to the standard middleware, the Referrer Policy, No Cache and Content Security Policy middlewares are used:

private setupSecurityMiddlewares(server: Express) {
    server.use(hpp())
    server.use(helmet())
    server.use(helmet.referrerPolicy({ policy: 'same-origin' }))
    server.use(helmet.noCache())
    server.use(helmet.contentSecurityPolicy({
        directives: {
            defaultSrc: ["'self'"],
            styleSrc: ["'unsafe-inline'"],
            scriptSrc: ["'unsafe-inline'", "'self'"]
        }
    }))
}

Other security measures for Express.js applications include the use of TLS, Anti CSRF (Cross Site Request Forgery) Tokens and sophisticated configuration of CORS (Cross-origin resource sharing). Recommended libraries to help are:

  • csurf – middleware for implementing Anti-CSRF Tokens
  • cors – middleware for configuring Cross-origin resource sharing
  • hpp – middleware for protection against HTTP Parameter Pollution (HPP) attacks

Rate Limiting in Express.js web applications

Rate limiting is helpful to rudimentarily protect critical API endpoints of the application from request hammering. For Express, the library express-rate-limit has proven itself due to its simplicity. By default, rate limiting is based on the IP addresses of users. This means that if a client with the same IP address has exhausted the rate limit, further requests are blocked by the server. Until the time window for this client permits requests again, the service responds to the client with the HTTP status code 429 “Too Many Requests”.

By default, the state via rate limiting is only kept in memory by the middleware. If accuracy of the limits is important, you should be aware that this rate limit counts per running instance. For example, if 3 instances are running and 100 requests per minute per IP address are allowed, this results in a total quota of 300 requests per minute for a client.

All instances addressed by the load balancer do rate limiting on their own and keep state only in-memory, so it is not synchronized between the instances.

All instances keep the rate limiting state only in the RAM. Counters diverge, at some point individual instances may block the client while others still permit requests.

In a highly elastic production environment, however, inaccuracies occur: Instances are subject to constantly scaling up and down and losing their rate limiting state. In such scenarios, Redis, Memcached or Mongo can also be used with the library as a store, so that the instances share their state via rate limiting.
If we need more specific rate limiting behavior, express-rate-limit offers more options. To automatically increase response times of the application instead of responding to requests with 429 “Too Many Requests”, express-slown-down can be used. If that’s not enough and we might want to implement a more complex API budget behavior or other stores for sharing the state between instances, it’s worth taking a look at rate-limiter-flexible.

All instances of the Express.js application keep the rate limiting state at the same location and are therefore synchronized.

All instances access the same persistent state during rate limiting, so that the instances can deterministically regulate the number of client requests.

On branch 06-web-security-and-rate-limiting rate limiting and basic web security were implemented by the presented middleware. The changes since the last update can be examined in pull request #5.

Feature Toggles in Express.js – Rolling out features on toggles and criteria

Feature Toggles (also called Feature Flags) are dynamically configurable switches that are used as Boolean expressions in code. They are used to decide whether to execute or skip code, or to decide which code to execute to offer a piece of functionality. This powerful technique allows you to deliberately change the behavior of the system with minimal intervention in the code. There are many scenarios in which they are of help. For example, teams can accomplish Trunk Based Development by bringing a feature’s code early into the main branch and thus into the production environment. With a feature toggle, this feature, which is still in development, is not activated until it is finished. Also when testing new features or technologies, feature toggles help activate code only for a part of the application users.
There are other interesting areas of use for feature toggles, as well as challenges associated with them, which are described in detail in an article by Pete Hodgson. Also, my colleague Mitchell Herrijgers wrote a series of articles about Feature Toggles in microservice environments which I recommend.

In the world of Node.js, the library fflip is suitable for lightweight definition of features and their execution criteria. The following code shows a minimal example:

import { Express, NextFunction, Response, Request } from 'express'
import * as fflip from 'fflip'
import * as FFlipExpressIntegration from 'fflip-express'

export const features: fflip.Feature[] = [
    { id: ‘CLOSED_BETA’,  criteria: { isPaidUser: true, shareOfUsers: 0.5 } },
    { id: ‘WITH_CAT_STATISTICS’,  enabled: true }
]
const criteria: fflip.Criteria[] = [
    {
        id: 'isPaidUser',
        check: (user: any, needsToBePaid: boolean) => user && user.isPaid === needsToBePaid
    },
    {
        id: 'shareOfUsers',
        check: (user: any, share: number) => user && user.id % 100 < share * 100
    }
]
export const applyFeatureToggles = (server: Express) => {
    fflip.config({ criteria, features })
    const fflipExpressIntegration = new FFlipExpressIntegration(fflip, {
        cookieName: 'fflip',
        manualRoutePath: '/api/toggles/local/:name/:action'
    })

    server.use(fflipExpressIntegration.middleware)
    server.use((req: Request, _: Response, next: NextFunction) => {
        req.fflip.setForUser(req.user)
        next()
    })
}

For Express.js there is also fflip-express. This can be used to create a middleware that can be mounted globally or only in special request mappings. The middleware then provides the method req.has(featureName) for implementing request processing. This method can be used to query the state of a feature in the code. It should be noted that feature toggles should be able to change their status during the lifecycle of the running application. So if a new feature offers a REST endpoint, the initially deactivated toggle should not exclude the instantiation of the request mapping (e.g. server.get('/api/cat/status', catEndpoints.catStatus)), but the logic executed within this function. The reason for this is that the instantiation of the request mapping is only executed once. This means that the feature could not be activated without a restart.

public getCatStatistics = async (req: Request, res: Response, next: NextFunction) => {
    try {
        if (req.fflip.has(FeatureToggles.WITH_CAT_STATISTICS)) {
            res.json(req.services.catService.getCatsStatistics())
        } else {
            res.sendStatus(HttpStatus.NOT_FOUND)
        }
    } catch (err) {
        next(err)
    }
}

“Binary features” such as withLandingPage from the previous code example cannot be changed during runtime by default. To activate the feature, the code must be changed and rolled out again. If you want to activate or deactivate features during the application’s runtime, the toggles’ state should be kept in database. Keeping the state of the toggles only in-memory of the application will make the state vanish on restarts. At the latest, if several instances are operated in parallel and traffic is routed “randomly” to instances, in-memory toggle state should be avoided because the memory of the instances is not synchronized. A simple key-value store or database should be used to keep the toggle state persistent across restarts and deployments. Unfortunately fflip does not offer this feature. As soon as you have special requirements (like the type of persistence of the state), you can implement it yourself. However, it would be easier to take a look at Unleash. Unleash offers an open-source server and client interfaces for feature toggles. You host the Unleash server including PostgreSQL database and choose the appropriate client library, in our case unleash-client-node, to communicate with the server. This variant offers everything you need, but can currently only work with PostgreSQL.

This cookbook only demonstrates the usage of fflip as a feature toggle library. On branch 07-fflip-feature-toggles a minimal demonstration has been implemented. Changes compared to the last branch are visible in this pull request.

Configuration of a web application in Node.js and Express.js

Configuration management can have a rather overblown meaning in IT. I don’t want to complicate it but show how much easier the topic can be approached in modern software development. The methodology “The Twelve-Factor App” offers a concise explanation of the topic of configuration. Essentially, it proposes to control configuration that changes across different deploys by using environment variables. Configuration should be strictly separated from code. The reason for this is that configuration changes across different environments (such as local/development environment, staging, production) – but code does not.

Node.js accesses environment variables via process.env. The ENVs are usually set by the operating environment, e.g. by the deployment mechanisms of Docker, Kubernetes, Nomad or other

Node.js accesses environment variables via process.env. The ENVs are usually set by the operating environment, e.g. by the deployment mechanisms of Docker, Kubernetes, Nomad etc. Environment variables are controlled per deploy by the development team.

Environment variables are ideally suited as a medium for this as they can be set and read out throughout programming languages and operating systems. Even in our Node-Express stack they don’t spoil the soup. The global object process.env contains all environment variables. For example, we can build a class that provides all configuration from the environment using static methods:

export class Environment {
    public static isLocal(): boolean {
        return Environment.getStage() === 'local'
    }
 
    public static isStaging(): boolean {
        return Environment.getStage() === 'staging'
    }
 
    public static isProd(): boolean {
        return Environment.getStage() === 'prod'
    }
 
    public static getStage(): string {
        return process.env.STAGE || 'local'
    }
 
    public static getPort(): number {
        return parseInt(process.env.PORT || '8000')
    }
}

Defining fallback values makes sense if there are environments (e.g. during local development) in which the environment variables are not set. Care should be taken not to define real secrets (usernames, passwords, tokens, certificates, etc.) as fallback values in code.
To illustrate the easy use of configurations from the environment, some new features have been implemented on branch 08-environment-configuration (comparison to previous step).

Glimpse into the alphabet soup

I hope that the second article in this series about Node, Express & TypeScript has enriched your repertoire of recipes and tools. With the help of the presented tricks, safer and easier-to-maintain software can be delivered. A strong knowledge of IT security, feature toggles and a clean approach to configuration are also very useful in any other stack.
According to the saying “A good cook must cost”, the Node ecosystem teaches us again that choosing the right libraries is invaluable. Express or Loopback? fflip or Unleash? The offer is large and it is often necessary to carefully weigh the options for solving a problem.

Recipes with the alphabet soup full of Node.js, Express.js and TypeScript

In the alphabet soup there were no “N” and “C” letters. Similar to what is sometimes necessary in the Node ecosystem, it was time for some pragmatism.

The next article continues with operationally relevant topics around Node.js and Express.js. This includes clustering, monitoring and containerization with Docker.

Jonas Verhoelen

Jonas is passionate about creating business value through software. He prefers to develop in Type- or JavaScript as well as various JVM languages and loves to work full-stack. Furthermore he provides expert knowledge about technology, methods and tools around IT Security and Distributed Ledger Technologies. Teamwork, self-organisation and an agile mindset are the basis of his daily work.

Comment

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