DEV Community

Dante
Dante

Posted on

1 1 1 1 1

Understanding Fastify Server Configuration: Best Practices for Production-Ready APIs

Understanding Fastify Server Configuration: Best Practices for Production-Ready APIs

Introduction

When building APIs with Fastify, proper server configuration is crucial for performance, security, and maintainability. In this post, we'll explore how Fastify manages server configuration, how to structure your configuration files, and best practices for different environments. We'll focus on the often-overlooked aspects of server options, npm scripts, and how they integrate with the autoloading pattern we discussed in my previous article.

The Basics of Fastify Server Configuration

Fastify offers extensive configuration options that control everything from validation behavior to logging levels. Let's start by understanding the standard ways to provide these options.

Configuration Through app.js

The most common approach is to export your configuration options from your main application file or a dedicated config file:

// app.js or server-options.js
module.exports.options = {
  // Server options here
}
Enter fullscreen mode Exit fullscreen mode

Understanding AJV Configuration

One of the most important configuration settings in Fastify relates to request validation through AJV (Another JSON Validator). Let's examine a common configuration:

module.exports.options = {
  ajv: {
    customOptions: {
      removeAdditional: 'all'
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

What does this do?

This configuration instructs AJV to strip any properties from incoming requests that aren't defined in your JSON schema. This single setting has significant implications:

  1. Security Enhancement: Prevents potentially malicious fields from reaching your handlers
  2. Data Cleansing: Automatically sanitizes incoming data to match your expected schema
  3. Reduced Validation Code: Eliminates the need to manually filter unwanted properties
  4. Predictable Data Structure: Ensures your handlers always receive the exact data structure you expect

AJV Configuration Options

While removeAdditional: 'all' is common, AJV offers several options for this setting:

  • 'all' - Remove all additional properties
  • true - Remove additional properties only if the schema has additionalProperties: false
  • 'failing' - Remove additional properties that would cause validation to fail
  • false (default) - Don't remove additional properties

Running Your Fastify Application

To start your application, you'll typically use npm scripts in your package.json:

{
  "scripts": {
    "start": "fastify start -l info --options app.js",
    "dev": "npm run start -- --watch --pretty-logs"
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's break down these commands:

Production Start Command

fastify start -l info --options app.js
Enter fullscreen mode Exit fullscreen mode
  • fastify start - Uses the fastify-cli to start your server
  • -l info - Sets logging level to "info" (alternatives: debug, error, fatal)
  • --options app.js - Tells Fastify to load options from app.js (or the path you specify)

Development Start Command

npm run start -- --watch --pretty-logs
Enter fullscreen mode Exit fullscreen mode
  • npm run start -- - Runs the start script while passing additional flags
  • --watch - Automatically restarts the server when files change
  • --pretty-logs - Formats logs for better readability during development

The Configuration Loading Lifecycle

Understanding how and when configuration is loaded can help you debug issues and structure your application more effectively.

Here's the typical lifecycle:

  1. CLI Arguments: Command-line flags take highest precedence
  2. Environment Variables: Environment variables are loaded
  3. Options File: The file specified by --options is loaded
  4. Default Options: Fastify's built-in defaults are applied for anything not specified

Connecting Configuration with Autoload

In our previous discussion on autoloading, we saw this pattern:

'use strict'

const path = require('path')
const AutoLoad = require('@fastify/autoload')

module.exports = async function (fastify, opts) {
  // Schemas, plugins, routes autoloaders...
}

module.exports.options = require('./configs/server-options')
Enter fullscreen mode Exit fullscreen mode

Now we can understand the full picture:

  1. The module.exports.options points to your server configuration
  2. When using fastify start --options app.js, these options are loaded
  3. The exported async function receives these options as the opts parameter
  4. The options are then passed down to autoloaded plugins and routes

This creates a clean flow of configuration from the top level down to every component.

Environment-Specific Configuration

In real-world applications, you'll want different configurations for development, testing, and production. Here's a pattern that works well with Fastify:

// configs/server-options.js
const envToConfig = {
  development: {
    logger: {
      level: 'debug',
      prettyPrint: true
    },
    ajv: {
      customOptions: {
        removeAdditional: 'all'
      }
    }
  },
  production: {
    logger: {
      level: 'info',
      prettyPrint: false
    },
    ajv: {
      customOptions: {
        removeAdditional: 'all'
      }
    }
  }
}

const env = process.env.NODE_ENV || 'development'
module.exports = envToConfig[env]
Enter fullscreen mode Exit fullscreen mode

Then update your npm scripts:

{
  "scripts": {
    "start": "NODE_ENV=production fastify start -l info --options app.js",
    "dev": "NODE_ENV=development npm run start -- --watch --pretty-logs"
  }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices for Server Configuration

Based on experience working with production Fastify applications, here are some recommended practices:

1. Centralize Configuration

Keep all your configuration in one place:

my-fastify-api/
  ├── configs/
  │   ├── server-options.js    <-- Main server options
  │   ├── database-config.js   <-- Database specific config
  │   └── auth-config.js       <-- Authentication config
  └── app.js                   <-- Imports and uses configs
Enter fullscreen mode Exit fullscreen mode

2. Use Environment Variables for Secrets

Never hardcode sensitive information:

module.exports.options = {
  database: {
    url: process.env.DATABASE_URL,
    password: process.env.DATABASE_PASSWORD
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Validate Your Configuration

Consider validating your configuration at startup:

const configSchema = {
  type: 'object',
  required: ['database', 'ajv'],
  properties: {
    database: {
      type: 'object',
      required: ['url'],
      properties: {
        url: { type: 'string' }
      }
    },
    ajv: { type: 'object' }
  }
}

module.exports = async function(fastify, opts) {
  // Validate config before proceeding
  const ajv = new Ajv()
  const validate = ajv.compile(configSchema)
  if (!validate(opts)) {
    throw new Error(`Invalid configuration: ${JSON.stringify(validate.errors)}`)
  }

  // Rest of your app...
}
Enter fullscreen mode Exit fullscreen mode

4. Document Your Configuration Options

Add comments to explain non-obvious options:

module.exports.options = {
  ajv: {
    customOptions: {
      // Strips undefined fields from requests
      // See: https://ajv.js.org/docs/options.html
      removeAdditional: 'all'
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Configuration Techniques

Composition Pattern

For complex applications, consider a composition pattern:

// configs/index.js
const base = require('./base-config')
const database = require('./database-config')
const auth = require('./auth-config')
const logger = require('./logger-config')

module.exports = {
  ...base,
  ...database,
  ...auth,
  ...logger
}
Enter fullscreen mode Exit fullscreen mode

Configuration Factory

For dynamic configurations, use a factory function:

// configs/server-options.js
module.exports = function createConfig(env) {
  const base = {
    // Common options
  }

  if (env === 'development') {
    return {
      ...base,
      // Development overrides
    }
  }

  return {
    ...base,
    // Production defaults
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Proper server configuration is the foundation of a robust Fastify application. By understanding how options are loaded and passed through your application, you can create a clean, maintainable configuration system.

The integration between your server options, the fastify-cli tool, and the autoloading pattern creates a powerful abstraction that allows your application to grow while keeping configuration centralized and manageable.

Remember that configuration is not just about getting your server to run—it's about creating a secure, performant, and maintainable system that can evolve with your needs.


Have you encountered any challenges with Fastify configuration? What patterns have worked well for your applications? Let me know in the comments below!

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (0)

Playwright CLI Flags Tutorial

5 Playwright CLI Flags That Will Transform Your Testing Workflow

  • 0:56 --last-failed
  • 2:34 --only-changed
  • 4:27 --repeat-each
  • 5:15 --forbid-only
  • 5:51 --ui --headed --workers 1

Learn how these powerful command-line options can save you time, strengthen your test suite, and streamline your Playwright testing experience. Click on any timestamp above to jump directly to that section in the tutorial!

Watch Full Video 📹️

👋 Kindness is contagious

If this article connected with you, consider tapping ❤️ or leaving a brief comment to share your thoughts!

Okay