CtroEnv
ctroenvType-Safe Environment Variables
Getting StartedQuick StartCore Concepts
defineEnv()string()number()boolean()pick()Chainable MethodsRefinementsError HandlingSchema Composition
CLI Overviewctroenv validatectroenv generatectroenv checkctroenv docsctroenv initCLI Configuration
Node AdapterVite AdapterNext.js Adapter
Migration from t3-envMigration from envalidMigration from dotenv

string()

Create a string validator with URL, email, regex, min, max, and secret checks.

  1. Docs
  2. Core API

string()

Creates a validator that accepts string values.

Signature

function string(): StringValidator

Accepted Values

Only actual string values pass the base validator. Non-strings produce a type error with a suggestion to wrap the value in quotes.

string().parse("hello")  // ✅ ok: "hello"
string().parse("")       // ✅ ok: ""
string().parse(42)       // ❌ type error: "Expected a string, received number"
string().parse(null)     // ❌ type error

Refinements

String refinements add additional constraints. Chain .min(), .max(), .url(), .email(), .port(), .regex() before .secret(), .optional(), .describe(). Reason: .secret() and .optional() return a generic wrapper that drops type-specific methods like .url().

.url()

Requires the value to be a valid URL (parsed via new URL()):

const env = defineEnv({
  API_URL: string().url(),
})
// ✅ "https://api.example.com/v1"
// ❌ "not-a-url"

.email()

Requires the value to match a basic email pattern:

const env = defineEnv({
  SUPPORT_EMAIL: string().email(),
})
// ✅ "user@example.com"
// ❌ "not-an-email"

Uses the regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/

.port()

Requires the value to be a valid port number string (1-65535):

const env = defineEnv({
  REDIS_PORT: string().port(),
})
// ✅ "6379"
// ✅ "65535"
// ❌ "0"
// ❌ "70000"

.min(length)

Requires the string to be at least length characters:

const env = defineEnv({
  PASSWORD: string().min(8),
})
// ✅ "abcdefgh" (8 characters)
// ❌ "abc" (too short)

.max(length)

Requires the string to be at most length characters:

const env = defineEnv({
  USERNAME: string().min(3).max(20),
})
// ✅ "john_doe"
// ❌ "ab" (too short)
// ❌ "a_very_long_username_that_exceeds" (too long)

.regex(pattern, message?)

Requires the string to match a regular expression:

const env = defineEnv({
  SLUG: string().regex(/^[a-z0-9-]+$/, "Must be a lowercase slug"),
})
// ✅ "my-blog-post"
// ❌ "Invalid slug!"

// Custom error message (optional second parameter)
string().regex(/^[A-Z]+$/, "Must be uppercase only")

return value: StringValidator

All refinement methods return a new StringValidator, enabling chaining:

string().min(1).max(255).url().optional()

The original validator is never mutated.

Examples

Basic string

const env = defineEnv({
  APP_NAME: string(),
})
// env.APP_NAME: string

String with constraints

const env = defineEnv({
  DATABASE_URL: string().url().describe("Primary database connection"),
  JWT_SECRET: string().min(32).secret(),
  NICKNAME: string().min(2).max(30).optional(),
  EMAIL: string().email(),
  PORT: string().port(),
})

Custom validation

const env = defineEnv({
  API_KEY: string().validate((value) => {
    if (!value.startsWith("sk_")) return "Must start with 'sk_'"
  }),
})

How is this guide?

Edit on GitHub

Last updated on Jun 24, 2026

PreviousdefineEnv()Nextnumber()

On this page

SignatureAccepted ValuesRefinements.url().email().port().min(length).max(length).regex(pattern, message?)return value: StringValidatorExamplesBasic stringString with constraintsCustom validation