Optimize DX with Typesafe Environment Variables and code Autocomplete
Tags: webdev, javascript, beginners, node
Published: 2024-07-08 09:10:39

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.

```sh
bun create hono@latest typesafe-env

```
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.
```sh

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"]

```

Install `zod`
```sh
bun add zod
```

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`

```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> {}
}

```


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<typeof envSchema>).

- `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`

```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,
};

```

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

[Repo link](https://github.com/harshmangalam/typesafe-env)