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
}
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'
}
}
}
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:
- Security Enhancement: Prevents potentially malicious fields from reaching your handlers
- Data Cleansing: Automatically sanitizes incoming data to match your expected schema
- Reduced Validation Code: Eliminates the need to manually filter unwanted properties
- 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 hasadditionalProperties: 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"
}
}
Let's break down these commands:
Production Start Command
fastify start -l info --options app.js
-
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
-
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:
- CLI Arguments: Command-line flags take highest precedence
- Environment Variables: Environment variables are loaded
-
Options File: The file specified by
--options
is loaded - 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')
Now we can understand the full picture:
- The
module.exports.options
points to your server configuration - When using
fastify start --options app.js
, these options are loaded - The exported async function receives these options as the
opts
parameter - 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]
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"
}
}
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
2. Use Environment Variables for Secrets
Never hardcode sensitive information:
module.exports.options = {
database: {
url: process.env.DATABASE_URL,
password: process.env.DATABASE_PASSWORD
}
}
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...
}
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'
}
}
}
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
}
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
}
}
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!
Top comments (0)