DEV Community

Cover image for Optimize DX with Typesafe Environment Variables and code Autocomplete
Harsh Mangalam
Harsh Mangalam

Posted on

Optimize DX with Typesafe Environment Variables and code Autocomplete

Hello! In this article, we'll guide you through configuring your codebase to achieve autocomplete for environment variables and ensure that missing variables trigger build-time errors.

Typesafe environment variables offer several benefits:

  • Reduced Errors: They help catch missing or incorrectly typed environment variables at compile time, reducing runtime errors.

  • Improved Developer Experience: Autocomplete support in IDEs speeds up development by providing hints and suggestions for environment variables.

  • Enhanced Code Readability: TypeScript's type annotations make it clear what type each environment variable should be, improving code clarity and maintainability.

  • Better Documentation: Type annotations serve as documentation, making it easier for developers to understand how to use environment variables correctly in different parts of the codebase.

In environment variables, we store sensitive data that should not be exposed in the codebase, such as JWT secrets, Database URLs, Stripe API keys, and other confidential information. This practice enhances security by preventing sensitive data from being hardcoded into the source code, thereby reducing the risk of exposure in case of unauthorized access or leaks.

By default, developers do not receive autocomplete support for process.env in Node.js, Bun.env in Bun runtime, or Deno.env.get() in Deno runtime. This lack of autocomplete can lead to manual errors and inefficiencies during development. Implementing typesafe handling of environment variables ensures that developers benefit from IDE autocomplete features, improving efficiency, reducing errors, and enhancing the overall development experience. This practice also promotes code clarity and maintainability by enforcing strict typing and documentation of environment variable usage throughout the codebase.

Let's set up a new server using the Hono framework.

bun create hono@latest typesafe-env

Enter fullscreen mode Exit fullscreen mode

Here I am using Bun because it doesn't require any third-party libraries to parse .env files. Bun has built-in features for parsing .env files, which makes configuration straightforward. Similarly, Deno also provides native capabilities for handling environment variables without additional dependencies.

If you are using Node.js, you will need third-party libraries like dotenv to parse .env files for environment variable configuration.

Bun can reads the following files automatically.

  • .env
  • .env.production, .env.development, .env.test (depending on value of NODE_ENV)
  • .env.local

Create new .env file and add all the env variables.


DATABASE_URL="postgresql://harshmangalam:123456@localahost:5432/typesafe-env?schema=default"
JWT_SECRET="itssecret!!"
PORT=4000
MAINTENANCE_MODE=false
DOMAINS=["http://localhost:3000","http://localhost:4000","https://openeventblend.com"]

Enter fullscreen mode Exit fullscreen mode

Install zod

bun add zod
Enter fullscreen mode Exit fullscreen mode

Zod is a TypeScript-first schema validation library that allows you to define and validate data schemas easily. It provides a way to create robust, composable, and typed schemas for your application's data, ensuring type safety and validation at runtime.

/src/config/env.ts


import * as z from "zod";

// Create zod schema for env variables
const envSchema = z.object({
  DATABASE_URL: z.string(),
  JWT_SECRET: z.string(),
  PORT: z.coerce.number().min(1000).max(65535),
  MAINTENANCE_MODE: z.coerce.boolean(),
});

export async function parseENV() {
  try {
    envSchema.parse(Bun.env);
  } catch (err) {
    console.error("Invalid Env variables Configuration::::", err);
    process.exit(1);
  }
}

declare module "bun" {
  interface Env extends z.TypeOf<typeof envSchema> {}
}

Enter fullscreen mode Exit fullscreen mode

This code snippet is a TypeScript declaration that extends the Env interface within the "bun" module or namespace. Here's a breakdown of what each part means:

  • declare module "bun": This declares a module or namespace named "bun" in TypeScript. Modules or namespaces in TypeScript are used to organize code and encapsulate its components.

  • interface Env extends z.TypeOf<typeof envSchema>: Within the "bun" module, this declares an interface named Env. It extends (extends) the type derived from a Zod schema (z.TypeOf).

  • z.TypeOf<typeof envSchema>: This is a TypeScript type operator provided by the Zod library. It retrieves the TypeScript type that corresponds to the Zod schema envSchema. Essentially, it defines the structure and types of environment variables as validated by the Zod schema.

This declaration allows TypeScript to enforce type checking and provide IntelliSense support for environment variables accessed through bun.Env, ensuring that they adhere to the structure defined by envSchema.

src/index.ts

import { Hono } from "hono";
import { parseENV } from "./config/env";

parseENV();

const app = new Hono();

app.get("/", (c) => {
  return c.text("Hello Hono!");
});

export default {
  fetch: app.fetch,
  port: Bun.env.PORT,
};

Enter fullscreen mode Exit fullscreen mode

Import parseENV into the root file, and you'll immediately benefit from autocomplete for Bun.env across all your project files.

Repo link

Top comments (2)

Collapse
 
philmillman profile image
Phil Miller

This is great!

We recently released dmno.dev to solve this problem as well. I haven't explicitly tested Bun/Deno + Hono but definitely check it out and let us know if it's helpful.

Collapse
 
harshmangalam profile image
Harsh Mangalam

Thanks @philmillman will try dmno.dev for bun.