Implementing type-checking for your environment variables.

·

3 min read

This article explores how to secure your environment variables with type safety using T3 Env, ensuring robust and error-free configuration.

Let's check out an example:

"use client"

import { ReactNode } from "react"
import { ConvexReactClient } from "convex/react"
import { ClerkProvider, useAuth } from "@clerk/nextjs"
import { ConvexProviderWithClerk } from "convex/react-clerk"

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL)

export default function ConvexClientProvider({
  children,
}: {
  children: ReactNode
}) {
  return (
    <ClerkProvider publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}>
      <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
        {children}
      </ConvexProviderWithClerk>
    </ClerkProvider>
  )
}

Our focus is these two lines;
1. const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL)
2. <ClerkProvider publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}>
where we are utilizing the process.env... to access the environment variables.

Sometimes, this can work, but also sometimes it will show an error like;
Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'.ts(2345) for this line const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL) .

Normally, how we fix this can be;
1. Using a non-null assertion operator (!)
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)
This tells TypeScript that you're certain that the value is not null/undefined.

This can lead to runtime error if the value is actually null or undefined .

2. Using type assertion
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL as string)
This tells TypeScript that you want to treat a value as a specific type, in this case, a string.

But there is a better way to fix this, validating your env. variables with Zod.

For this example, we'll focus on Next.js usage;

  1. Install necessary dependencies
    pnpm add @t3-oss/env-nextjs zod

  2. Creating the schema
    For instance src/lib/env.ts or you can also split between two files - one for the server, e.g, src/lib/server.ts and the other for the client, e.g, src/lib/client.ts .

     import { createEnv } from "@t3-oss/env-nextjs"
     import { z } from "zod"
    
     export const env = createEnv({
       server: {
         CLERK_SECRET_KEY: z.string().min(1),
         CLERK_JWT_ISSUER_DOMAIN: z.string().url(),
       },
       client: {
         NEXT_PUBLIC_CONVEX_URL: z.string().url(),
         NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
       },
       runtimeEnv: {
         CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
         CLERK_JWT_ISSUER_DOMAIN: process.env.CLERK_JWT_ISSUER_DOMAIN,
         NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL,
         NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
           process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
       },
     })
    
  3. Validating schema on build

    This is to ensure that your env. variables are validated at build time. So ensure you've installed jiti - pnpm add jiti .
    Put the changes in the next.config.mjs file;

     import createJiti from "jiti"
     import { fileURLToPath } from "node:url"
     const jiti = createJiti(fileURLToPath(import.meta.url))
    
     jiti("./src/lib/env") // file location of the schema
    
     /** @type {import('next').NextConfig} */
     const nextConfig = {
       ...
     }
    
     export default nextConfig
    
    1. Utilizing the schema
    "use client"

    import { ReactNode } from "react"
    import { ConvexReactClient } from "convex/react"
    import { ClerkProvider, useAuth } from "@clerk/nextjs"
    import { ConvexProviderWithClerk } from "convex/react-clerk"

    import { env } from "@/src/lib/env" // importing the schema

    const convex = new ConvexReactClient(env.NEXT_PUBLIC_CONVEX_URL)

    export default function ConvexClientProvider({
      children,
    }: {
      children: ReactNode
    }) {
      return (
        <ClerkProvider
          publishableKey={env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
        >
          <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
            {children}
          </ConvexProviderWithClerk>
        </ClerkProvider>
      )
    }

Now you can import the env object in your application and use it - with type-safety and auto-completion.