Implementing type-checking for your environment variables.
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 assertionconst 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;
Install necessary dependencies
pnpm add @t3-oss/env-nextjs zod
Creating the schema
For instancesrc/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, }, })
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 thenext.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
- 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.