Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Bun .env for API Keys & Config

Welcome back. In our last session, we built a complete run-debug cycle for our project, enabling us to execute and inspect TypeScript code efficiently with Bun. This sets the stage for writing and testing our bot logic.

Now, we'll address the final, critical piece of our project setup: configuration. Any application, especially a bot connecting to an external service, needs to manage sensitive information like API tokens and environment-specific settings like database URLs or log levels. This lesson focuses on how to manage these environment variables using Bun's powerful, built-in support for .env files. You'll learn how this native feature simplifies a common development task and how to integrate it with TypeScript for a fully type-safe configuration.

1. Native Support for .env Files

A cornerstone of modern application development is separating configuration from code. In the Node.js ecosystem, this is typically handled by the dotenv package, which loads variables from a .env file into the process environment.

Bun streamlines this by baking .env file support directly into its runtime. This means you don't need to install or configure any external packages; Bun automatically finds and loads these files when your application starts.

The "Bun Crash Course" video provides a succinct demonstration of this feature.

Bun Crash Course | JavaScript Runtime, Bundler & Transpiler

This clip from the Traversy Media course shows how Bun automatically picks up variables from a .env file.

Watch the segment from this section on environment variables. Note how a .env file is created and its PORT variable is immediately accessible in the code without any extra setup.

Now, let's apply this to our project. For our Telegram bot, the most crucial piece of configuration will be the bot's API token.

Hands-on: Creating Your .env File

  1. In the root of your project, create a new file named .env.

  2. Add the following line to it. We will get a real token from BotFather in a later module, so for now, we'll use a placeholder.

    BOT_TOKEN="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
    
  3. Crucially, add .env to your .gitignore file. You must never commit files containing secrets to version control.

    
    
    
    # .gitignore
    
    node_modules
    .env
    

2. Accessing and Typing Environment Variables

With Bun, environment variables are loaded into a global object. You can access them in a few ways. The official documentation provides a comprehensive overview.

Environment Variables - Bun

This official guide details how to access variables and, importantly for us, how to make them type-safe.

First, read the section on how to read variables, which introduces process.env, Bun.env, and import.meta.env. Then, pay close attention to the TypeScript section. This explains the default string | undefined type and how to use interface merging to provide strong types for your configuration, which is a significant advantage in a TypeScript project.

As the documentation explains, you can use:

  • process.env.YOUR_VARIABLE: This ensures compatibility with existing Node.js code and libraries.
  • Bun.env.YOUR_VARIABLE: This is the idiomatic Bun approach. It's an alias for process.env but can be slightly more performant as it avoids the Node.js compatibility layer.

By default, TypeScript treats any property on these objects as string | undefined. This is unsafe because it forces you to handle the undefined case everywhere, even when you know a variable must be present for the application to function.

We can fix this using TypeScript's declaration merging. By augmenting Bun's built-in Env interface, we can inform TypeScript about our specific environment variables, their existence, and their types.

Hands-on: Type-Safe Configuration

  1. Create a new file in your project's root directory (or a types subdirectory) named env.d.ts. The .d.ts extension marks it as a type declaration file.

  2. Add the following code to env.d.ts:

    declare module "bun" {
      interface Env {
        BOT_TOKEN: string;
      }
    }
    

    This globally merges our BOT_TOKEN property into Bun's Env interface, typing it as a non-optional string. Your IDE's TypeScript server will pick this up automatically.

  3. Now, modify your index.ts file to access this variable:

    import { Bot } from "grammy";
    
    // Access the BOT_TOKEN from the environment
    const botToken = Bun.env.BOT_TOKEN;
    
    if (!botToken) {
      throw new Error("BOT_TOKEN must be provided!");
    }
    
    console.log("Successfully loaded BOT_TOKEN.");
    // We'll use this token to initialize the bot later.
    
  4. Run the file: bun index.ts. You should see the success message.

  5. Hover over Bun.env.BOT_TOKEN in your editor. You'll see that it is now correctly typed as string, not string | undefined. You will also get autocompletion when typing Bun.env., as shown in the image below.

This image shows the autocompletion provided by a code editor once environment variables are properly typed. This eliminates guesswork and typos when accessing configuration.

3. Managing Multiple Environments

Most applications operate in different environments (development, production, testing), each requiring its own configuration. Bun supports this with a clear file precedence system.

How to Use Environment Variables in Bun

This article from OneUptime provides a practical overview of managing environment-specific files.

Read the section on Working with .env Files. It details the loading order and gives clear examples of using .env, .env.local, and environment-specific files like .env.development.

Bun loads .env files in the following order, with variables in later files overriding those in earlier ones:

  1. .env (Base configuration, committed to git)
  2. .env.[development|production|test] (Environment-specific, committed to git)
  3. .env.local (Local overrides, not committed to git)

The environment is determined by the NODE_ENV environment variable. Let's see this in practice.

Hands-on: Development-Specific Configuration

  1. Add a new variable to your .env file:
    BOT_TOKEN="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
    LOG_LEVEL="info"
    
  2. Create a new file named .env.development and add:
    LOG_LEVEL="debug"
    
  3. Update your env.d.ts file to include the new variable:
    declare module "bun" {
      interface Env {
        BOT_TOKEN: string;
        LOG_LEVEL: "info" | "debug" | "warn" | "error";
      }
    }
    
  4. Modify index.ts to print the log level:
    // ... previous code ...
    console.log(`Current log level: ${Bun.env.LOG_LEVEL}`);
    
  5. Update your package.json dev script to set NODE_ENV:
    "scripts": {
      "start": "bun index.ts",
      "dev": "NODE_ENV=development bun --watch index.ts"
    },
    
  6. Run bun run dev. You will see Current log level: debug, because NODE_ENV=development caused Bun to load .env.development, overriding the value from .env.
  7. Stop the process and run bun run start. You will see Current log level: info, as NODE_ENV was not set and only the base .env file was used.

4. Validation and Best Practices

For a production-ready application, relying only on type declarations is insufficient. You should actively validate your configuration at startup to ensure all required variables are present. This "fail-fast" approach prevents obscure runtime errors later on.

The OneUptime article outlines several excellent patterns for building a robust configuration module.

How to Use Environment Variables in Bun

This resource offers robust patterns for professional-grade configuration management.

Skim through the sections on creating a typed config, handling required variables, and the best practices summary. You don't need to implement all of this now, but it's valuable to understand these patterns, such as creating helper functions to parse and validate variables at application startup.

A simple but effective pattern is to centralize your environment variable access in a single file that validates and exports them. This encapsulates all your configuration logic. For our project, the check we already added in index.ts serves as a basic form of this validation:

const botToken = Bun.env.BOT_TOKEN;

if (!botToken) {
  throw new Error("BOT_TOKEN must be provided!");
}

This ensures your application will immediately crash with a clear error message if the bot token is missing, which is far better than failing later when it tries to connect to the Telegram API.

Conclusion

You have now mastered a fundamental aspect of application development in the Bun ecosystem. By leveraging Bun's native support for .env files and integrating it with TypeScript's type system, you can create configuration that is both simple to manage and robustly safe.

Key Takeaways:

  • Bun automatically loads environment variables from .env files, eliminating the need for packages like dotenv.
  • Variables are accessible via Bun.env (idiomatic) or process.env (for compatibility).
  • You can achieve full type safety and editor autocompletion by augmenting the Bun.Env interface in a .d.ts declaration file.
  • Bun supports environment-specific files (e.g., .env.development) that are loaded based on the NODE_ENV variable, allowing for clean separation of concerns.
  • Validating required variables at startup is a critical best practice to ensure application stability.

With our project structure, dependencies, run-debug cycle, and configuration all in place, our foundation is complete. In the next lesson, we will explore the last core feature of the Bun runtime relevant to our goal: its highly optimized, built-in fetch API for making external HTTP requests. This will be the final step before we dive into the Telegram Bot API in the next module.

Can't find a good explanation? Sign up and we'll make it for you

Sign up