Type-safe environment variables on both client and server with Remix
Tags: remix, typescript, react, advanced
Published: 2022-11-14 14:09:20

> If you are running Remix on Vite, I have [an updated post](https://dev.to/seasonedcc/type-safe-env-vars-in-remix-a-modern-approach-with-arktype-11k9) with a simpler, leaner way to get env vars on the client. Check it out!

In this post we are going to use TypeScript to:
- strongly type our `process.env` variables
- expose some public variables from the server to the client, keeping autocomplete all the way

**In case you just want to check the final solution, the source code is at the end of the post**

As with most Remix tutorials, the contents of this post are not coupled to Remix in any way. If you want to expose env variables from the server to the client this approach might be suitable to your app.

## Baseline
Let's say we have some environment variables in our `.env` file:

```sh
# .env
SESSION_SECRET="s3cret"
GOOGLE_MAPS_API_KEY="somekey"
STRIPE_PUBLIC_KEY="should be exposed"
STRIPE_SECRET_KEY="shouldn't be exposed"
```
We want to expose some of them to the client so we can use some client libraries. All the secret tokens should dwell on the server-side only, but we can safely expose `STRIPE_PUBLIC_KEY` and `GOOGLE_MAPS_API_KEY`.

If you don't know how to expose environment variables to the browser with Remix, I recommend you to stop here and take a quick look at the [official approach](https://remix.run/docs/en/v1/guides/envvars#browser-environment-variables) or even [Sergio's post](https://sergiodxa.com/articles/use-process-env-client-side-with-remix) prior to reading this post.

By following that approach we end up with this code:

```tsx
// app/root.tsx
export function loader() {
  return json({
    publicKeys: {
      GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY,
      STRIPE_PUBLIC_KEY: process.env.STRIPE_PUBLIC_KEY,
    }
  })
}

export default () => {
  const { publicKeys } = useLoaderData<typeof loader>()
  return (
    <html lang="en">
      <body>
        <script
          dangerouslySetInnerHTML={{
            __html: `window.ENV = ${JSON.stringify(publicKeys)}`,
          }}
        />
        {/* ... App ... */}
        <Scripts />
      </body>
    </html>
  )
}
```

And it works at runtime, yay!

However, on behalf of all the _typescript junkies_ out there, we can't stop here as we shall have type-safety and autocomplete ~~for breakfast~~ for both client and server.

Take a look at types inferred from `process.env`:
![Wide process.env](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nwd5ab0kqtservpdhxmi.png)

**We can do better than that!**


## Type-safe process.env
IMO there's no better way to validate data structures on both type level and runtime as [Zod](https://github.com/colinhacks/zod). So let's go ahead and install it:

```sh
npm install --save zod
```

Now we can express our `.env` file in a zod schema and have a function to return the parsed values from that file. Create a new file as follows:
```ts
// app/environment.server.ts
import * as z from 'zod'

const environmentSchema = z.object({
  NODE_ENV: z
    .enum(['development', 'production', 'test'])
    .default('development'),
  SESSION_SECRET: z.string().min(1),
  GOOGLE_MAPS_API_KEY: z.string().min(1),
  STRIPE_PUBLIC_KEY: z.string().min(1),
  STRIPE_SECRET_KEY: z.string().min(1),
})

const environment = () => environmentSchema.parse(process.env)

export { environment }
```

And then update the `root.tsx`'s loader function to use that new helper:
```ts
// app/root.tsx
export function loader() {
  return json({
    publicKeys: {
      GOOGLE_MAPS_API_KEY: environment().GOOGLE_MAPS_API_KEY,
      STRIPE_PUBLIC_KEY: environment().STRIPE_PUBLIC_KEY,
    },
  })
}
```

By doing so, we've achieved some benefits:
- We now have strongly typed environment variables at the server-side
- We have a nice documentation of `.env` file and we can now ditch the `.env.sample` file
- Since peers won't run the app without the required env vars, there will be no surprises at runtime

Check out how much narrower our environment variables got:
![Type-save process.env](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/liygiqxk64xwxkm5cdyo.gif)

The problem is that we are exposing some variables to the browser but TypeScript doesn't know about them yet:
![window.ENV is not type-safe](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n1rglq130xy2sjhfvqd8.png)

Let's fix it.

## Type-safe window.ENV
To let TypeScript know that our `window` now has access to those values, we can extend the global `Window` interface:
```ts
declare global {
  interface Window {
    ENV: {
      GOOGLE_MAPS_API_KEY: string
      STRIPE_PUBLIC_KEY: string
    }
  }
}
```
And we now have code completion for the `window.ENV` variables:
![Type safe window.ENV](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bcdk20ozxu67p219n90y.gif)

## The `<PublicEnv />` component
Let's create a new React component to keep all the logic related to browser environment variables in a single file.

We can even bring the global declaration here and it will work across the whole app:
```tsx
// app/ui/public-env.tsx
declare global {
  interface Window {
    ENV: Props
  }
}
type Props = {
  GOOGLE_MAPS_API_KEY: string
  STRIPE_PUBLIC_KEY: string
}
function PublicEnv(props: Props) {
  return (
    <script
      dangerouslySetInnerHTML={{
        __html: `window.ENV = ${JSON.stringify(props)}`,
      }}
    />
  )
}
```
Notice the `Props` type has the exact values we want to expose to the `window`.

Now we update our `root.tsx` file to use that component instead of the `<script />` tag:
```tsx
// app/root.tsx
export default () => {
  const { publicKeys } = useLoaderData<typeof loader>()
  return (
    <html lang="en">
      <body>
        <PublicEnv {...publicKeys} />
      </body>
    </html>
  )
}
```

As you can see, everything is strongly typed:
![Using the PublicEnv](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5kiopqed5lrwsjrz3uxr.gif)

However, if we run the code above (with the `console.log`) we are going to see an error:
![window not defined](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4isn2eyr3virmooon0fe.png)

As you might've guessed, we can't access `window` on the server - not talking about you, Deno 🦕 👀 - the same way we can't access `process.env` in the browser.

## Universal environment variables
I'm not sure I like this term but `universal` has been used quite often to mean a piece of code that can run on both client and server seamlessly.

We need a function that returns us the contents of `window.ENV` when the runtime does have access to `window` or the contents of `process.env` otherwise. Let's create it in the same file as the `<PublicEnv />` component:
```ts
// app/ui/public-env.tsx
function getPublicEnv<T extends keyof Props>(key: T): Props[T] {
  return typeof window === 'undefined'
    ? environment()[key]
    : window.ENV[key]
}
```
By using a generic `T extends keyof Props` we get code completion when calling `getPublicEnv`:
![code completion on getPublicEnv](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8d4vraw1gfkjwwr5lpe5.gif)

But if we tell TS that `window.ENV` is available without actually checking it we are lying to ourselves. What if someone removes the `<PublicEnv />` component from the `root.tsx` or places the component below the `<Scripts />` component?

Let's add that check:
```ts
// app/ui/public-env.tsx
function getPublicEnv<T extends keyof Props>(key: T): Props[T] {
  if (typeof window !== 'undefined' && !window.ENV) {
    throw new Error(
      `Missing the <PublicEnv /> component at the root of your app.`,
    )
  }

  return typeof window === 'undefined'
    ? environment()[key]
    : window.ENV[key]
}
```
Now we throw a friendly error message to our fellow peers in case they mess up with our dear component:
![Missing the <PublicEnv /> component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ajzc3edia80vc69ltt0y.png)

**Strongly-typed universal environment variables** unlocked, yay!
![Universal env vars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wirmpq40f7zdwoyva71h.gif)

## Final touches
There's still one thing that bothers me. We must keep track of the exposed env vars in two places: the loader function of `root.tsx` and the `Props` type of `public-env.tsx`. Whenever we change one of them we have to tweak the other.

Let's take care of that.

We are going to need a function to `pick` which variables of our `environment` are going to make it to the browser. Let's create that function:
```ts
function typedPick<T extends {}, U extends Array<keyof T>>(obj: T, keys: U) {
  let result = {} as Pick<T, U[number]>
  for (const key of keys) {
    result[key] = obj[key]
  }
  return result
}
```

Then, we create a function to return only the env vars that are to be exposed. We co-locate it with the `environment` function so we keep a single file responsible for that subject:
```ts
// app/environment.server.ts

function getPublicKeys() {
  return {
    publicKeys: typedPick(environment(), [
      'STRIPE_PUBLIC_KEY',
      'GOOGLE_MAPS_API_KEY',
    ]),
  }
}
```
Update the `root.tsx`'s loader function to use `getPublicKeys`:
```ts
// app/root.tsx
export function loader() {
  return json(getPublicKeys())
}
```

And we can now derive the `<PublicEnv />`'s `Props` type out of that new function. Let's update that file:
```ts
// app/ui/public-env.tsx
type Props = ReturnType<typeof getPublicKeys>['publicKeys']
declare global {
  interface Window { ENV: Props }
}
```

Done! We now have a single source of truth for our universal environment variables.

Check out the resulting DX:
![Final result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i19uox30fb3jkmlzhvtp.gif)

We change one single file and let TypeScript have our backs throughout the App.

## That's it 🔥
Let me know what you think about this post in the comments below. I'll be happy to get some feedback, help, and suggestions.

Remix is still quite new and we are figuring stuff out on the go, but I can't help but smile whenever I realize that finding out how to do something with Remix is actually finding out how to do something on the web 🤓

## Check the [repo on github](https://github.com/gustavoguichard/remix-public-env)