FastAPI Config: Serve Configuration Files Via API

by Alex Johnson 50 views

In this comprehensive guide, we will explore how to build a FastAPI application that serves configuration files. This setup allows you to retrieve configuration files stored on your server from anywhere using a simple curl command. Using FastAPI for this purpose provides a robust, efficient, and modern way to manage and distribute your configuration files. This approach ensures that your applications can dynamically fetch the latest configurations, making your system more flexible and maintainable.

Setting Up the FastAPI Project Structure

To begin, let's set up the project structure. A well-organized project structure is crucial for maintainability and scalability. Here’s a recommended structure:

config_api/
β”œβ”€β”€ main.py
β”œβ”€β”€ config/
β”‚   β”œβ”€β”€ config1.json
β”‚   β”œβ”€β”€ config2.yaml
β”‚   └── ...
└── utils.py
  • main.py: This is the main file where the FastAPI application will be defined.
  • config/: This directory will hold all your configuration files in various formats such as .json, .yaml, .ini, etc.
  • utils.py: This file can contain utility functions, such as functions to read and parse configuration files.

Step-by-Step Implementation

  1. Install FastAPI and Uvicorn

    First, you need to install FastAPI and Uvicorn, an ASGI server, which will run your application. Open your terminal and run:

pip install fastapi uvicorn pyyaml ```

Here, `pyyaml` is included to handle `.yaml` configuration files. You can install other necessary libraries based on the types of configuration files you intend to support.
  1. Create Configuration Files

    Create a config directory and add some sample configuration files. For example, let's create config1.json and config2.yaml.

    config/config1.json:

    {
      "setting1": "value1",
      "setting2": 123,
      "setting3": true
    }
    

    config/config2.yaml:

    setting4: "value4"
    setting5: 456
    setting6: false
    
  2. Implement Utility Functions in utils.py

    Create a utils.py file to handle reading the configuration files. This file will contain functions to load .json and .yaml files.

    import json
    import yaml
    from pathlib import Path
    
    def read_json_config(file_path: str) -> dict:
        try:
            with open(file_path, 'r') as f:
                return json.load(f)
        except FileNotFoundError:
            raise FileNotFoundError(f"Configuration file not found: {file_path}")
        except json.JSONDecodeError:
            raise ValueError(f"Invalid JSON format in: {file_path}")
    
    def read_yaml_config(file_path: str) -> dict:
        try:
            with open(file_path, 'r') as f:
                return yaml.safe_load(f)
        except FileNotFoundError:
            raise FileNotFoundError(f"Configuration file not found: {file_path}")
        except yaml.YAMLError:
            raise ValueError(f"Invalid YAML format in: {file_path}")
    
    def read_config(file_path: str) -> dict:
        file_path = Path(file_path)
        if file_path.suffix == '.json':
            return read_json_config(str(file_path))
        elif file_path.suffix == '.yaml' or file_path.suffix == '.yml':
            return read_yaml_config(str(file_path))
        else:
            raise ValueError(f"Unsupported configuration file format: {file_path.suffix}")
    

    This utils.py file includes functions to read both JSON and YAML configuration files. The read_config function automatically determines the file type based on the file extension and calls the appropriate reading function. Error handling is included to manage file not found scenarios and invalid file formats.

  3. Create the Main FastAPI Application in main.py

    Now, let’s create the main FastAPI application in main.py. This application will define the API endpoints to serve the configuration files.

    from fastapi import FastAPI, HTTPException
    from fastapi.responses import JSONResponse
    from utils import read_config
    import os
    
    app = FastAPI()
    
    CONFIG_DIR = "config"
    
    @app.get("/config/{filename}")
    async def get_config(filename: str):
        file_path = os.path.join(CONFIG_DIR, filename)
        try:
            config_data = read_config(file_path)
            return JSONResponse(content=config_data)
        except FileNotFoundError:
            raise HTTPException(status_code=404, detail="Configuration file not found")
        except ValueError as e:
            raise HTTPException(status_code=400, detail=str(e))
    

    In this code:

    • We import necessary modules from FastAPI and the utils.py file.
    • We initialize a FastAPI app.
    • We define a CONFIG_DIR variable to specify the directory where configuration files are stored.
    • We create an endpoint /config/{filename} that takes the filename as a parameter.
    • The get_config function reads the configuration file using the read_config function from utils.py.
    • If the file is found and the format is valid, the function returns the configuration data as a JSON response.
    • If the file is not found, it raises an HTTP 404 error. If the file format is invalid, it raises an HTTP 400 error.

Running the Application

To run the application, open your terminal, navigate to the project directory, and run the following command:

uvicorn main:app --reload

This command starts the Uvicorn server, hosting your FastAPI application. The --reload flag allows the server to automatically reload whenever you make changes to the code.

Testing the Endpoint with curl

Now that the application is running, you can test the endpoint using curl. Open a new terminal and run the following commands:

To fetch config1.json:

curl http://localhost:8000/config/config1.json

Expected output:

{"setting1": "value1", "setting2": 123, "setting3": true}

To fetch config2.yaml:

curl http://localhost:8000/config/config2.yaml

Expected output:

{"setting4": "value4", "setting5": 456, "setting6": false}

If you try to fetch a non-existent file, such as config3.json, you will receive an HTTP 404 error:

curl http://localhost:8000/config/config3.json

Expected output:

{"detail":"Configuration file not found"}

Advanced Features and Improvements

1. Environment Variable Configuration

To make the application more configurable, you can use environment variables. For example, you can set the CONFIG_DIR environment variable to specify the directory where configuration files are stored. Modify the main.py file as follows:

import os

CONFIG_DIR = os.environ.get("CONFIG_DIR", "config")

Now, the application will first check if the CONFIG_DIR environment variable is set. If it is, it will use that value. Otherwise, it will default to the config directory.

2. Adding Validation

To ensure that the configuration files are valid, you can add validation using libraries like Pydantic. First, install Pydantic:

pip install pydantic

Then, create a Pydantic model to define the structure of your configuration files. For example, create a config_model.py file:

from pydantic import BaseModel

class ConfigModel(BaseModel):
    setting1: str
    setting2: int
    setting3: bool

Modify the utils.py file to validate the configuration data against the Pydantic model:

from pydantic import ValidationError
from config_model import ConfigModel

def read_json_config(file_path: str) -> dict:
    try:
        with open(file_path, 'r') as f:
            data = json.load(f)
            ConfigModel(**data)
            return data
    except FileNotFoundError:
        raise FileNotFoundError(f"Configuration file not found: {file_path}")
    except json.JSONDecodeError:
        raise ValueError(f"Invalid JSON format in: {file_path}")
    except ValidationError as e:
        raise ValueError(f"Configuration validation error: {e}")

Now, if the configuration file does not match the structure defined in the ConfigModel, a validation error will be raised.

3. Using API Keys for Authentication

To secure your configuration API, you can use API keys for authentication. Modify the main.py file to include API key authentication:

from fastapi import Security, HTTPException
from fastapi.security.api_key import APIKeyHeader, APIKey

API_KEY = "your_secret_api_key"
API_KEY_NAME = "X-API-Key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=True)

async def get_api_key(api_key_header: str = Security(api_key_header)) -> str:
    if api_key_header == API_KEY:
        return api_key_header
    else:
        raise HTTPException(status_code=403, detail="Could not validate API key")

@app.get("/config/{filename}", dependencies=[Security(get_api_key)])
async def get_config(filename: str):
    # ... (rest of the code)

Now, you need to include the X-API-Key header in your curl requests:

curl -H "X-API-Key: your_secret_api_key" http://localhost:8000/config/config1.json

If the API key is incorrect or missing, you will receive an HTTP 403 error.

Conclusion

In this guide, you've learned how to create a FastAPI application to serve configuration files. This setup provides a flexible and efficient way to manage and distribute configurations to your applications. By incorporating advanced features such as environment variables, validation, and API key authentication, you can further enhance the robustness and security of your configuration API.

By following these steps, you can easily manage and serve configuration files using FastAPI, making your applications more flexible and maintainable.

For further reading on FastAPI and its capabilities, visit the official FastAPI Documentation.