DevToolBoxFREE
Blog

String Case Converter: Convert camelCase, snake_case, kebab-case Online — Complete Guide

12 min readby DevToolBox

TL;DR

A string case converter transforms identifiers between naming conventions. Use camelCase for JS variables and functions, PascalCase for React components and classes, snake_case for Python and PostgreSQL, kebab-case for CSS and URLs, and SCREAMING_SNAKE_CASE for constants. Install the change-case npm library for JavaScript or humps for Python to handle all conversions reliably. Try our free online string case converter for instant transformations, or follow the code examples below for manual implementations.

The 9 Most Common String Case Types — With Examples

Every programming language and ecosystem has its own conventions for naming variables, functions, classes, files, and database columns. Understanding which case to use and when prevents naming conflicts, improves readability, and integrates seamlessly with existing codebases and tooling.

Case TypeExamplePatternPrimary Use
camelCasemyVariableNameLowercase start, uppercase word boundariesJS/TS variables, Java fields, JSON keys
PascalCaseMyClassNameUppercase every word boundary including firstReact components, TypeScript classes, C# types
snake_casemy_variable_nameLowercase, underscores between wordsPython, Ruby, PostgreSQL, Rust, PHP
kebab-casemy-variable-nameLowercase, hyphens between wordsCSS classes, HTML attributes, URLs, npm packages
SCREAMING_SNAKEMY_CONSTANT_NAMEUppercase, underscores between wordsConstants, environment variables, macros
dot.casemy.config.keyLowercase, dots between wordsConfig keys (Spring, .properties files)
Title CaseMy Variable NameCapitalize first letter of each wordUI headings, article titles, menu items
UPPERCASEMYVARIABLENAMEAll uppercase, no separatorsSQL keywords, short acronyms (HTML, CSS, JSON)
lowercasemyvariablenameAll lowercase, no separatorsSingle-word identifiers, simple keys

When to Use Each Case — Conventions by Language and Framework

Using the wrong naming convention is not just a style issue — it causes real problems. Python linters flag camelCase variables, PostgreSQL lowercases unquoted identifiers making camelCase columns invisible, and CSS parsers reject underscore-separated class names in some contexts. Here is the definitive guide to which case belongs where.

JavaScript and TypeScript

// camelCase: variables, functions, method names, object properties
const userName = 'alice';
const maxRetryCount = 3;
function getUserProfile(userId: string) { ... }
const apiResponse = { firstName: 'Alice', lastName: 'Smith' };

// PascalCase: classes, React components, TypeScript interfaces, enums
class UserService { ... }
function ProfileCard({ user }: { user: User }) { ... }
interface HttpResponse { statusCode: number; body: string; }
enum Direction { North, South, East, West }

// SCREAMING_SNAKE_CASE: constants (module-level or readonly)
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL!;
const DEFAULT_TIMEOUT_MS = 5000;

// kebab-case: NOT valid as JS identifiers, but used in:
// - CSS module class names: styles['card-header']
// - HTML element IDs: document.getElementById('main-nav')
// - npm package names: import('change-case')
// - CLI flags: --output-dir, --max-connections

Python (PEP 8)

# snake_case: variables, functions, methods, module names
user_name = 'alice'
max_retry_count = 3

def get_user_profile(user_id: str) -> dict:
    ...

# PascalCase: classes (the only exception to snake_case in Python)
class UserService:
    def get_user(self, user_id: str): ...

class HttpError(Exception):
    pass

# SCREAMING_SNAKE_CASE: module-level constants
MAX_FILE_SIZE = 10 * 1024 * 1024
API_BASE_URL = os.environ['API_BASE_URL']
DEFAULT_TIMEOUT_MS = 5000

# _single_leading_underscore: "internal use" convention
_cache = {}

# __double_leading_underscore: name mangling in classes
class MyClass:
    __private_attr = 'cannot easily access from subclass'

CSS and HTML

/* kebab-case is the universal CSS convention */
.nav-bar { display: flex; }
.card-header { font-weight: bold; }
.btn-primary { background: blue; }

/* HTML attributes: kebab-case */
<div data-user-id="123" aria-label="Close dialog">
<button class="btn-primary" id="submit-form">Submit</button>

/* CSS Custom Properties (variables) — also kebab-case */
:root {
  --color-primary: #3b82f6;
  --font-size-base: 16px;
  --border-radius-lg: 8px;
}

/* BEM methodology: block__element--modifier */
.card { }               /* block */
.card__header { }       /* element */
.card__header--sticky { } /* modifier */
.card--featured { }     /* block modifier */

/* Tailwind utility classes — all kebab-case */
/* text-gray-500, bg-blue-600, rounded-lg, flex-col, px-4 */

Databases — PostgreSQL, MySQL, SQLite

-- PostgreSQL: snake_case is mandatory for unquoted identifiers
-- PostgreSQL lowercases ALL unquoted identifiers
CREATE TABLE user_accounts (
    user_id       SERIAL PRIMARY KEY,
    email_address VARCHAR(255) UNIQUE NOT NULL,
    created_at    TIMESTAMP DEFAULT NOW(),
    is_active     BOOLEAN DEFAULT TRUE
);

-- WRONG: This looks like camelCase but PostgreSQL treats it as lowercase
-- SELECT userId FROM users;  -- actually selects "userid" column!
-- You must quote to preserve case:
-- SELECT "userId" FROM users;  -- preserved, but ugly

-- MySQL: case-insensitive on most platforms (Windows), case-sensitive on Linux
-- Best practice: always use snake_case to avoid cross-platform issues
SELECT user_name, email_address FROM user_accounts WHERE is_active = 1;

-- SQL keywords: UPPERCASE (convention, not required)
SELECT, FROM, WHERE, JOIN, ON, GROUP BY, HAVING, ORDER BY, LIMIT

JavaScript — Manual Case Conversion Functions

Sometimes you need lightweight conversions without installing a library. These battle-tested functions handle the most common scenarios including acronyms, numbers, and edge cases.

Core Conversion Functions

/**
 * Splits any cased string into individual words.
 * Handles camelCase, PascalCase, snake_case, kebab-case, spaces, dots.
 */
function splitWords(str: string): string[] {
  return str
    // Insert space before uppercase letters that follow lowercase letters or digits
    // Handles camelCase: "helloWorld" => "hello World"
    .replace(/([a-zd])([A-Z])/g, '$1 $2')
    // Insert space before uppercase letters followed by lowercase (handles acronyms)
    // "XMLParser" => "XML Parser"
    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
    // Replace all non-alphanumeric characters with spaces
    .replace(/[s_-.]+/g, ' ')
    .trim()
    .toLowerCase()
    .split(/s+/)
    .filter(Boolean); // remove empty strings
}

// camelCase: "hello-world_foo" => "helloWorldFoo"
function toCamelCase(str: string): string {
  const words = splitWords(str);
  return words[0] + words.slice(1).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
}

// PascalCase: "hello-world_foo" => "HelloWorldFoo"
function toPascalCase(str: string): string {
  return splitWords(str).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
}

// snake_case: "helloWorldFoo" => "hello_world_foo"
function toSnakeCase(str: string): string {
  return splitWords(str).join('_');
}

// kebab-case: "helloWorldFoo" => "hello-world-foo"
function toKebabCase(str: string): string {
  return splitWords(str).join('-');
}

// SCREAMING_SNAKE_CASE: "helloWorldFoo" => "HELLO_WORLD_FOO"
function toConstantCase(str: string): string {
  return splitWords(str).join('_').toUpperCase();
}

// dot.case: "helloWorldFoo" => "hello.world.foo"
function toDotCase(str: string): string {
  return splitWords(str).join('.');
}

// Title Case: "hello-world" => "Hello World"
function toTitleCase(str: string): string {
  return splitWords(str).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
}

// Sentence case: "HELLO_WORLD" => "Hello world"
function toSentenceCase(str: string): string {
  const words = splitWords(str);
  return words[0].charAt(0).toUpperCase() + words[0].slice(1) + ' ' + words.slice(1).join(' ');
}

// Test all conversions
const input = 'XMLParser_config-KEY';
console.log(toCamelCase(input));    // => "xmlParserConfigKey"
console.log(toPascalCase(input));   // => "XmlParserConfigKey"
console.log(toSnakeCase(input));    // => "xml_parser_config_key"
console.log(toKebabCase(input));    // => "xml-parser-config-key"
console.log(toConstantCase(input)); // => "XML_PARSER_CONFIG_KEY"
console.log(toDotCase(input));      // => "xml.parser.config.key"
console.log(toTitleCase(input));    // => "Xml Parser Config Key"
console.log(toSentenceCase(input)); // => "Xml parser config key"

Regex Explanation — How Case Detection Works

The key to reliable case conversion is the splitWords function. Let's break down each regex step:

// Step 1: Handle camelCase boundaries
// Pattern: ([a-zd])([A-Z])
// Matches a lowercase letter or digit followed by an uppercase letter
// "helloWorld" => "hello World", "getHTTP" stays (handled in step 2)
.replace(/([a-zd])([A-Z])/g, '$1 $2')

// Step 2: Handle acronyms (consecutive uppercase letters)
// Pattern: ([A-Z]+)([A-Z][a-z])
// Matches: one or more uppercase + uppercase followed by lowercase
// "XMLParser" => "XML Parser" (split before the "Pa" in Parser)
// "HTTPSRequest" => "HTTPS Request"
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')

// Step 3: Replace non-word characters
// Pattern: [s_-.]+
// Handles: underscores, hyphens, dots, spaces, and combinations
// "hello__world" => "hello world", "foo.bar-baz" => "foo bar baz"
.replace(/[s_-.]+/g, ' ')

// Why filter(Boolean)?
// After splitting, empty strings can appear at the start/end
// "  hello  world  ".split(/s+/) => ['', 'hello', 'world', '']
.filter(Boolean)

// Digit boundary handling:
// "base64Encoder" => ["base", "64", "encoder"] — keep digits with adjacent word
// Current pattern: ([a-zd])([A-Z]) treats digits like lowercase letters
// So "base64Encoder" => "base64 Encoder" => ["base64", "encoder"]

JavaScript — The change-case Library (npm)

For production use, the change-case library is the gold standard. It handles all edge cases including acronyms, unicode characters, locale-specific casing, and digit boundaries — and it is fully typed for TypeScript.

# Install the library
npm install change-case

# TypeScript types are included — no @types package needed
import {
  camelCase,
  pascalCase,
  snakeCase,
  kebabCase,
  constantCase,
  dotCase,
  pathCase,
  titleCase,
  sentenceCase,
  capitalCase,
  noCase,
} from 'change-case';

const input = 'hello world foo-bar';

// Core cases
console.log(camelCase(input));     // "helloWorldFooBar"
console.log(pascalCase(input));    // "HelloWorldFooBar"
console.log(snakeCase(input));     // "hello_world_foo_bar"
console.log(kebabCase(input));     // "hello-world-foo-bar"
console.log(constantCase(input));  // "HELLO_WORLD_FOO_BAR"

// Less common cases
console.log(dotCase(input));       // "hello.world.foo.bar"
console.log(pathCase(input));      // "hello/world/foo/bar"
console.log(titleCase(input));     // "Hello World Foo Bar"
console.log(sentenceCase(input));  // "Hello world foo bar"
console.log(capitalCase(input));   // "Hello World Foo Bar" (same as title)
console.log(noCase(input));        // "hello world foo bar" (just lowercased, space-separated)

// Handles mixed input flawlessly
console.log(snakeCase('XMLParser'));      // "xml_parser"
console.log(camelCase('HTTPSRequest'));   // "httpsRequest"
console.log(kebabCase('base64Encode'));   // "base-64-encode"
console.log(pascalCase('MY_CONSTANT'));  // "MyConstant"

// Custom split function for special behavior
import { camelCase, Options } from 'change-case';

const opts: Options = {
  // Preserve acronyms as-is
  splitRegexp: /([a-z])([A-Z])/g,
};
console.log(camelCase('XMLParser', opts)); // "xmlParser" vs "xMLParser" without custom split

Practical change-case Patterns

import { camelCase, snakeCase, kebabCase, pascalCase } from 'change-case';

// Convert object keys recursively
function camelizeKeys<T>(obj: unknown): T {
  if (Array.isArray(obj)) {
    return obj.map(camelizeKeys) as T;
  }
  if (obj !== null && typeof obj === 'object') {
    return Object.fromEntries(
      Object.entries(obj as Record<string, unknown>).map(([key, value]) => [
        camelCase(key),
        camelizeKeys(value),
      ])
    ) as T;
  }
  return obj as T;
}

// Convert object keys to snake_case (for sending to Python API)
function snakelizeKeys<T>(obj: unknown): T {
  if (Array.isArray(obj)) {
    return obj.map(snakelizeKeys) as T;
  }
  if (obj !== null && typeof obj === 'object') {
    return Object.fromEntries(
      Object.entries(obj as Record<string, unknown>).map(([key, value]) => [
        snakeCase(key),
        snakelizeKeys(value),
      ])
    ) as T;
  }
  return obj as T;
}

// Usage: API returns snake_case, convert to camelCase for React state
const apiData = {
  user_name: 'alice',
  email_address: 'alice@example.com',
  profile_settings: { theme_color: '#3b82f6', font_size: 14 }
};

const normalized = camelizeKeys(apiData);
// { userName: 'alice', emailAddress: 'alice@example.com', profileSettings: { themeColor: '#3b82f6', fontSize: 14 } }

// Form data to API (camelCase UI -> snake_case for server)
const formData = { firstName: 'Alice', emailAddress: 'alice@example.com' };
const apiPayload = snakelizeKeys(formData);
// { first_name: 'Alice', email_address: 'alice@example.com' }

Python — String Methods and Case Conversion

Python's built-in string methods handle the simple cases. For complex conversions involving mixed input, the re module and the third-party humps library cover all scenarios.

Built-in String Methods

# Python built-in string methods for basic case changes
text = "hello world"

# Basic transformations
print(text.lower())       # "hello world"
print(text.upper())       # "HELLO WORLD"
print(text.title())       # "Hello World"
print(text.capitalize())  # "Hello world" (only first letter)
print(text.swapcase())    # "HELLO WORLD" -> "hello world" (inverts case)

# title() has a quirk: capitalizes after ANY non-letter
print("it's a trap".title())    # "It'S A Trap" (apostrophe triggers cap)
print("don't panic".title())    # "Don'T Panic" -- WRONG

# Better title case with str.capwords()
import string
print(string.capwords("it's a trap"))  # "It's A Trap"
print(string.capwords("don't panic"))  # "Don't Panic"

# Check case type
print("helloWorld".islower())    # True
print("HELLO".isupper())         # True
print("Hello World".istitle())   # True

Custom Conversion Functions with re module

import re

def camel_to_snake(name: str) -> str:
    """Convert camelCase or PascalCase to snake_case."""
    # Insert underscore before uppercase letters that follow lowercase/digits
    # "helloWorld" => "hello_World" => "hello_world"
    s1 = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name)
    # Insert underscore before uppercase letters that follow lowercase (handles acronyms)
    # "XMLParser" => "XML_Parser" => "xml_parser"
    return re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s1).lower()

def snake_to_camel(name: str) -> str:
    """Convert snake_case to camelCase."""
    components = name.split('_')
    return components[0] + ''.join(x.title() for x in components[1:])

def snake_to_pascal(name: str) -> str:
    """Convert snake_case to PascalCase."""
    return ''.join(x.title() for x in name.split('_'))

def to_kebab(name: str) -> str:
    """Convert any case to kebab-case."""
    # First convert to snake_case, then replace underscores with hyphens
    snake = camel_to_snake(name)
    return snake.replace('_', '-')

def to_constant(name: str) -> str:
    """Convert any case to SCREAMING_SNAKE_CASE."""
    return camel_to_snake(name).upper()

# Test
print(camel_to_snake('helloWorld'))        # "hello_world"
print(camel_to_snake('XMLParser'))         # "xml_parser"
print(camel_to_snake('HTTPSConnection'))   # "https_connection"
print(camel_to_snake('getHTTPResponse'))   # "get_http_response"

print(snake_to_camel('hello_world'))       # "helloWorld"
print(snake_to_camel('xml_parser_config')) # "xmlParserConfig"

print(snake_to_pascal('user_profile'))     # "UserProfile"
print(to_kebab('helloWorldFoo'))           # "hello-world-foo"
print(to_constant('apiBaseUrl'))           # "API_BASE_URL" (approximate)

Python — humps Library for Dict Key Conversion

# Install: pip install pyhumps
import humps

# Single string conversions
print(humps.camelize('hello_world'))        # "helloWorld"
print(humps.decamelize('helloWorld'))       # "hello_world"
print(humps.pascalize('hello_world'))       # "HelloWorld"
print(humps.kebabize('hello_world'))        # "hello-world"

# Recursive dict key conversion — the killer feature
snake_dict = {
    'user_name': 'alice',
    'email_address': 'alice@example.com',
    'profile_settings': {
        'theme_color': '#3b82f6',
        'font_size': 14
    },
    'recent_posts': [
        {'post_title': 'Hello World', 'created_at': '2026-02-27'}
    ]
}

camel_dict = humps.camelize(snake_dict)
# {
#   'userName': 'alice',
#   'emailAddress': 'alice@example.com',
#   'profileSettings': { 'themeColor': '#3b82f6', 'fontSize': 14 },
#   'recentPosts': [{ 'postTitle': 'Hello World', 'createdAt': '2026-02-27' }]
# }

# Convert back to snake_case
back_to_snake = humps.decamelize(camel_dict)
# Exactly matches original snake_dict

# FastAPI + Pydantic v2 with automatic camelCase conversion
from pydantic import BaseModel, ConfigDict
from humps import camelize

class UserProfile(BaseModel):
    model_config = ConfigDict(alias_generator=camelize, populate_by_name=True)
    user_name: str
    email_address: str

# Now accepts both:
# { "user_name": "alice" }  (snake_case)
# { "userName": "alice" }   (camelCase alias)
profile = UserProfile.model_validate({'userName': 'alice', 'emailAddress': 'a@b.com'})
print(profile.user_name)  # "alice"
print(profile.model_dump(by_alias=True))  # { 'userName': ..., 'emailAddress': ... }

Go — Unicode-Aware Case Conversion

Go's standard library provides basic string case operations. For more sophisticated unicode-aware conversions, the golang.org/x/text/cases package handles locale-specific casing rules correctly.

package main

import (
    "strings"
    "unicode"
    "fmt"
    "regexp"
    "golang.org/x/text/cases"
    "golang.org/x/text/language"
)

// Basic Go string case operations (standard library)
func basicCases() {
    s := "Hello World"
    fmt.Println(strings.ToLower(s))  // "hello world"
    fmt.Println(strings.ToUpper(s))  // "HELLO WORLD"
    fmt.Println(strings.Title(s))    // "Hello World" (deprecated, use cases package)
}

// Unicode-aware title case (golang.org/x/text/cases)
// go get golang.org/x/text
func unicodeTitleCase(s string, lang language.Tag) string {
    caser := cases.Title(lang)
    return caser.String(s)
}

// camelCase to snake_case conversion in Go
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var matchAllCap   = regexp.MustCompile("([a-z0-9])([A-Z])")

func toSnakeCase(str string) string {
    snake := matchFirstCap.ReplaceAllString(str, "1_2")
    snake  = matchAllCap.ReplaceAllString(snake, "1_2")
    return strings.ToLower(snake)
}

// snake_case to camelCase
func toCamelCase(str string) string {
    parts := strings.Split(str, "_")
    result := parts[0] // keep first part as-is (lowercase start)
    for _, part := range parts[1:] {
        if len(part) > 0 {
            result += strings.ToUpper(part[:1]) + part[1:]
        }
    }
    return result
}

// snake_case to PascalCase
func toPascalCase(str string) string {
    parts := strings.Split(str, "_")
    result := ""
    for _, part := range parts {
        if len(part) > 0 {
            result += strings.ToUpper(part[:1]) + part[1:]
        }
    }
    return result
}

// Check if rune is uppercase
func isUpper(r rune) bool {
    return unicode.IsUpper(r)
}

func main() {
    fmt.Println(toSnakeCase("CamelCaseString"))     // "camel_case_string"
    fmt.Println(toSnakeCase("HTMLParser"))           // "html_parser"
    fmt.Println(toCamelCase("hello_world"))          // "helloWorld"
    fmt.Println(toPascalCase("user_profile"))        // "UserProfile"

    // Unicode title case (English)
    enCaser := cases.Title(language.English)
    fmt.Println(enCaser.String("hello world"))       // "Hello World"

    // Turkish locale handles ı/İ correctly
    trCaser := cases.Title(language.Turkish)
    fmt.Println(trCaser.String("istanbul"))          // "İstanbul" (dotted I)
}

Database Column Naming — ORM Case Conversion

The mismatch between database snake_case column names and application camelCase variables is one of the most common sources of friction in backend development. ORMs handle this automatically with proper configuration.

SQLAlchemy (Python)

# SQLAlchemy with snake_case column names (Python convention)
from sqlalchemy import Column, String, Integer, DateTime, Boolean
from sqlalchemy.orm import DeclarativeBase, mapped_column, Mapped
from datetime import datetime

class Base(DeclarativeBase):
    pass

class UserAccount(Base):
    __tablename__ = 'user_accounts'

    user_id: Mapped[int] = mapped_column(Integer, primary_key=True)
    email_address: Mapped[str] = mapped_column(String(255), unique=True)
    display_name: Mapped[str] = mapped_column(String(100))
    is_active: Mapped[bool] = mapped_column(Boolean, default=True)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

# Pydantic schema for API serialization (camelCase for JSON output)
from pydantic import BaseModel, ConfigDict
from humps import camelize

class UserAccountResponse(BaseModel):
    model_config = ConfigDict(
        from_attributes=True,  # Allow ORM object -> Pydantic
        alias_generator=camelize,
        populate_by_name=True,
    )
    user_id: int
    email_address: str
    display_name: str
    is_active: bool
    created_at: datetime

# SQLAlchemy model -> Pydantic schema -> JSON output
user = db.query(UserAccount).first()
response = UserAccountResponse.model_validate(user)
print(response.model_dump(by_alias=True))
# { "userId": 1, "emailAddress": "alice@example.com", "displayName": "Alice", ... }

Prisma ORM (TypeScript)

// Prisma schema: snake_case in database, camelCase in TypeScript
// schema.prisma
model UserAccount {
  user_id       Int      @id @default(autoincrement()) @map("user_id")
  email_address String   @unique @map("email_address")
  display_name  String   @map("display_name")
  is_active     Boolean  @default(true) @map("is_active")
  created_at    DateTime @default(now()) @map("created_at")

  @@map("user_accounts")
}

// Prisma auto-generates TypeScript types with camelCase:
// { userId: number; emailAddress: string; displayName: string; ... }

// Usage in TypeScript — automatically camelCase
const user = await prisma.userAccount.findFirst({
  where: { isActive: true },  // camelCase in TypeScript
  select: {
    userId: true,
    emailAddress: true,
    displayName: true,
  }
});
// Generated SQL: SELECT user_id, email_address, display_name FROM user_accounts WHERE is_active = true

// Drizzle ORM — explicit column naming
import { pgTable, serial, varchar, boolean, timestamp } from 'drizzle-orm/pg-core';

export const userAccounts = pgTable('user_accounts', {
  userId: serial('user_id').primaryKey(),         // TS field: userId, DB column: user_id
  emailAddress: varchar('email_address', { length: 255 }).unique(),
  displayName: varchar('display_name', { length: 100 }),
  isActive: boolean('is_active').default(true),
  createdAt: timestamp('created_at').defaultNow(),
});

API Response Case Conversion — Axios Interceptors and Fetch Middleware

The most common real-world case conversion challenge: a Python or Ruby backend sends JSON with snake_case keys, but your JavaScript/TypeScript frontend expects camelCase. Handle this systematically with interceptors instead of converting keys ad-hoc throughout your codebase.

Axios Interceptors

import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { camelCase, snakeCase } from 'change-case';

// Generic recursive key transformer
function transformKeys(
  obj: unknown,
  transform: (key: string) => string
): unknown {
  if (Array.isArray(obj)) {
    return obj.map(item => transformKeys(item, transform));
  }
  if (obj !== null && typeof obj === 'object' && !(obj instanceof Date)) {
    return Object.fromEntries(
      Object.entries(obj as Record<string, unknown>).map(([key, value]) => [
        transform(key),
        transformKeys(value, transform),
      ])
    );
  }
  return obj;
}

const camelizeKeys = (obj: unknown) => transformKeys(obj, camelCase);
const snakelizeKeys = (obj: unknown) => transformKeys(obj, snakeCase);

// Create axios instance with automatic case conversion
export const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: { 'Content-Type': 'application/json' },
});

// Response interceptor: snake_case -> camelCase
api.interceptors.response.use(
  (response: AxiosResponse) => {
    if (response.data && typeof response.data === 'object') {
      response.data = camelizeKeys(response.data);
    }
    return response;
  },
  error => Promise.reject(error)
);

// Request interceptor: camelCase -> snake_case
api.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    if (config.data && typeof config.data === 'object') {
      config.data = snakelizeKeys(config.data);
    }
    return config;
  },
  error => Promise.reject(error)
);

// Now all requests/responses automatically handle case conversion
const { data } = await api.get<UserProfile>('/api/users/123');
// data.userName, data.emailAddress — already camelCase

await api.post('/api/users', { firstName: 'Alice', emailAddress: 'a@b.com' });
// Sends: { first_name: 'Alice', email_address: 'a@b.com' }

Fetch API Middleware

import { camelCase, snakeCase } from 'change-case';

// Re-use key transformers from above
function transformKeys(obj: unknown, fn: (k: string) => string): unknown { ... }
const camelizeKeys = (obj: unknown) => transformKeys(obj, camelCase);
const snakelizeKeys = (obj: unknown) => transformKeys(obj, snakeCase);

// fetch wrapper with automatic case conversion
async function fetchApi<T>(
  url: string,
  options: RequestInit = {}
): Promise<T> {
  // Convert request body keys to snake_case
  if (options.body && typeof options.body === 'string') {
    try {
      const parsed = JSON.parse(options.body);
      options.body = JSON.stringify(snakelizeKeys(parsed));
    } catch { /* not JSON, leave as-is */ }
  }

  const response = await fetch(url, {
    ...options,
    headers: { 'Content-Type': 'application/json', ...options.headers },
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  const json = await response.json();
  return camelizeKeys(json) as T;
}

// Usage — fully typed, automatic case conversion
interface UserProfile {
  userId: number;
  userName: string;
  emailAddress: string;
}

const user = await fetchApi<UserProfile>('/api/users/123');
console.log(user.userName); // camelCase, regardless of what server sent

File Naming Conventions — By Language and Ecosystem

File naming conventions are enforced by linters, build tools, and team conventions. Using the wrong convention can cause case-sensitive import failures on Linux servers even when the code works on macOS.

EcosystemConventionExamples
React componentsPascalCase.tsxUserProfile.tsx, NavBar.tsx, ProductCard.tsx
React hookscamelCase.tsuseAuth.ts, useDebounce.ts, useLocalStorage.ts
CSS Moduleskebab-case.module.cssuser-profile.module.css, nav-bar.module.css
Next.js routeskebab-case/app/user-profile/page.tsx, app/blog-posts/[slug]/page.tsx
Python modulessnake_case.pyuser_service.py, auth_middleware.py, db_models.py
Java classesPascalCase.javaUserService.java, HttpController.java
Go packageslowercaseuserservice.go, httphandler.go (single word preferred)
Rust filessnake_case.rsuser_service.rs, http_client.rs, lib.rs
npm packageskebab-casechange-case, react-router-dom, next-auth
# ESLint rule to enforce file naming conventions
# .eslintrc.json
{
  "rules": {
    // Enforce PascalCase for React component files
    "@typescript-eslint/naming-convention": [
      "error",
      {
        "selector": "default",
        "format": ["camelCase"]
      },
      {
        "selector": "typeLike",
        "format": ["PascalCase"]
      },
      {
        "selector": "enumMember",
        "format": ["UPPER_CASE"]
      }
    ]
  }
}

# Case-sensitivity trap on macOS vs Linux:
# macOS: case-insensitive by default (HFS+)
# Linux: case-sensitive (ext4)

# These import successfully on macOS but FAIL on Linux:
import UserProfile from './userprofile';  // Wrong case
import NavBar from './Navbar';            // Wrong case

# Always match the exact filename case:
import UserProfile from './UserProfile';  // Correct
import NavBar from './NavBar';            // Correct

CSS Class Naming — BEM, Tailwind, SMACSS Patterns

CSS class naming methodologies provide structure and predictability. The naming convention you choose determines how readable and maintainable your stylesheets remain as the project grows.

BEM — Block Element Modifier

/* BEM naming: block__element--modifier */
/* All lowercase, kebab-case for multi-word segments */

/* Block — standalone component */
.card { }
.nav-bar { }
.search-form { }

/* Element — part of a block, separated by double underscore */
.card__title { }
.card__body { }
.card__footer { }
.nav-bar__logo { }
.nav-bar__menu-item { }  /* multi-word element: kebab within segment */
.search-form__input { }
.search-form__submit-btn { }

/* Modifier — variant of block or element, separated by double hyphen */
.card--featured { }           /* block modifier */
.card--dark-theme { }         /* multi-word modifier */
.card__title--large { }       /* element modifier */
.nav-bar__menu-item--active { }  /* element + modifier */
.btn--primary { }
.btn--sm { }
.btn--disabled { }

/* Usage in HTML */
<div class="card card--featured">
  <h2 class="card__title card__title--large">Featured Article</h2>
  <div class="card__body">Content here</div>
  <footer class="card__footer">
    <button class="btn btn--primary">Read More</button>
  </footer>
</div>

Tailwind CSS — Utility-First (All kebab-case)

/* Tailwind uses kebab-case for all utilities */
/* Pattern: {property}-{value} or {breakpoint}:{property}-{value} */

/* Spacing: p-4 = padding: 1rem, mx-auto = margin-left/right: auto */
/* Typography: text-xl, font-bold, text-gray-500, tracking-wide */
/* Layout: flex, grid, grid-cols-3, gap-4, flex-col */
/* Responsive: sm:text-lg md:grid-cols-2 lg:max-w-4xl */
/* State: hover:bg-blue-600 focus:ring-2 active:scale-95 */
/* Dark mode: dark:bg-gray-800 dark:text-white */

/* Component example using Tailwind */
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
  <h2 className="mb-2 text-xl font-bold text-gray-900">Card Title</h2>
  <p className="text-gray-500">Card body text</p>
  <button className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
    Action
  </button>
</div>

/* Custom Tailwind utilities — still kebab-case */
/* tailwind.config.ts */
theme: {
  extend: {
    colors: {
      'brand-primary': '#3b82f6',       /* class: text-brand-primary */
      'brand-secondary': '#8b5cf6',     /* class: bg-brand-secondary */
    },
    spacing: {
      '18': '4.5rem',    /* class: p-18, m-18 */
      '128': '32rem',    /* class: max-w-128 */
    }
  }
}

URL Slug Generation — Handling Unicode, Accents, and Edge Cases

A URL slug is a URL-safe string representation of a title or name. Slugs must be lowercase, hyphen-separated, and contain only ASCII-safe characters. This requires handling accented characters, special symbols, apostrophes, and leading/trailing hyphens.

// Manual slug generation — handles common cases
function slugify(str: string): string {
  return str
    .toLowerCase()
    // Normalize unicode: decompose accents (é -> e + combining accent)
    .normalize('NFD')
    // Remove combining diacritical marks (accents, umlauts, etc.)
    .replace(/[̀-ͯ]/g, '')
    // Replace & with 'and'
    .replace(/&/g, '-and-')
    // Replace remaining non-alphanumeric with hyphens
    .replace(/[^a-z0-9s-]/g, '')
    // Replace spaces and multiple hyphens with single hyphen
    .replace(/[s_-]+/g, '-')
    // Remove leading/trailing hyphens
    .replace(/^-+|-+$/g, '');
}

slugify('Hello World')                   // "hello-world"
slugify("What's New in React 19?")       // "whats-new-in-react-19"
slugify('Café & Restaurant Review')      // "cafe-and-restaurant-review"
slugify('Ñoño español')                  // "nono-espanol"
slugify('  --Leading Hyphens--  ')       // "leading-hyphens"
slugify('C++ Programming Guide')         // "c-programming-guide"
slugify('Node.js vs Deno vs Bun 2026')   // "nodejs-vs-deno-vs-bun-2026"
# npm install slugify — handles even more edge cases
import { default as slugifyFn } from 'slugify';

slugifyFn('Hello World', { lower: true })
// "hello-world"

slugifyFn('Ñoño español', { lower: true, locale: 'es' })
// "nonno-espanol" (locale-aware transliteration)

slugifyFn('Üniversität München', { lower: true, locale: 'de' })
// "universitaet-muenchen" (ü -> ue in German)

slugifyFn('日本語タイトル', { lower: true })
// "" — non-Latin scripts need romanization first

# Python: pip install python-slugify
from slugify import slugify

slugify('Hello World')                  # "hello-world"
slugify('Café & Restaurant')           # "cafe-restaurant"
slugify('Ñoño español')                # "nono-espanol"

# With custom separator
slugify('hello world', separator='_')  # "hello_world"

# Preserve unicode (for non-Latin scripts)
slugify('日本語', allow_unicode=True)   # "日本語"

# Max length for database columns
slugify('Very Long Title That Exceeds The Limit', max_length=30)
# "very-long-title-that-exceeds"

Regular Expressions for Case Conversion — Deep Dive

Understanding the regex patterns behind case conversion helps you handle edge cases correctly and debug unexpected behavior in existing tools. The key challenges are word boundary detection, acronym handling, and digit boundaries.

Word Boundary Patterns

// Pattern 1: Detect camelCase boundaries
// Matches: lowercase or digit immediately followed by uppercase
// "helloWorld" => positions: [5] (between 'o' and 'W')
const camelBoundary = /([a-zd])([A-Z])/g;

// Pattern 2: Detect acronym boundaries
// Matches: sequence of uppercase followed by uppercase+lowercase
// "XMLParser" => "XML" + "Parser"
// "HTTPSRequest" => "HTTPS" + "Request"
const acronymBoundary = /([A-Z]+)([A-Z][a-z])/g;

// Pattern 3: Non-word separators (underscores, hyphens, dots, spaces)
const separators = /[s_-.]+/g;

// Combining all three for complete word splitting:
function getWordBoundaries(str: string): string[] {
  return str
    .replace(camelBoundary, '$1 $2')    // Step 1: camelCase
    .replace(acronymBoundary, '$1 $2')  // Step 2: acronyms
    .replace(separators, ' ')           // Step 3: separators
    .trim()
    .toLowerCase()
    .split(' ')
    .filter(Boolean);
}

// Walkthrough with "getHTTPSResponse":
// After step 1: "get H T T P S Response"? No — let's trace carefully
// Input: "getHTTPSResponse"
// Step 1 (camelBoundary): matches 't' followed by 'H' => "get HTTPSResponse"
//   Also matches 'S' followed by 'R'? No — 'S' is uppercase, not matched by [a-zd]
// After step 1: "get HTTPSResponse"
// Step 2 (acronymBoundary): matches "HTTPS" followed by "Re" (R is uppercase, e is lowercase)
//   => "get HTTPS Response"
// After step 2: "get HTTPS Response"
// Step 3: no separators to replace
// Lowercase + split: ["get", "https", "response"]
// Result: getHTTPSResponse => ["get", "https", "response"]

console.log(getWordBoundaries('getHTTPSResponse'));  // ["get", "https", "response"]
console.log(getWordBoundaries('XMLParser'));          // ["xml", "parser"]
console.log(getWordBoundaries('base64Encode'));       // ["base64", "encode"]
console.log(getWordBoundaries('user_name-v2'));       // ["user", "name", "v2"]

Acronym Handling Strategies

// Different teams handle acronyms differently. Know your team's convention:

// Strategy 1: Always lowercase (most common, what change-case does)
// "XMLParser" => "xml_parser" (snake), "xmlParser" (camel), "XmlParser" (pascal)
// "HTTPSRequest" => "https_request", "httpsRequest", "HttpsRequest"

// Strategy 2: Preserve acronym in UPPER (uncommon, harder to implement)
// "XMLParser" => "XML_Parser" (snake), "XMLParser" (camel), "XMLParser" (pascal)
// This requires knowing which words are acronyms

// Strategy 3: Minimal split (split only on case transitions)
// "XMLParser" is kept as one word if no transition detected

// The change-case library uses Strategy 1 by default:
import { snakeCase, camelCase, pascalCase } from 'change-case';

console.log(snakeCase('XMLParser'));      // "xml_parser"
console.log(snakeCase('HTTPSRequest'));   // "https_request"
console.log(snakeCase('getHTTPSResponse')); // "get_https_response"
console.log(camelCase('xml_parser'));     // "xmlParser"
console.log(pascalCase('xml_parser'));    // "XmlParser"

// Custom acronym list (if you need Strategy 2):
const KNOWN_ACRONYMS = new Set(['xml', 'http', 'https', 'api', 'url', 'id', 'uuid', 'html', 'css', 'sql']);

function toSnakeCaseWithAcronyms(str: string): string {
  const words = getWordBoundaries(str);
  return words.map(w => KNOWN_ACRONYMS.has(w) ? w.toUpperCase() : w).join('_');
}

console.log(toSnakeCaseWithAcronyms('getHTTPSResponse')); // "get_HTTPS_response"
console.log(toSnakeCaseWithAcronyms('parseXMLData'));     // "parse_XML_data"

TypeScript — Template Literal Types for Case Conversion

TypeScript 4.1 introduced template literal types and built-in intrinsic string types that enable type-level case manipulation. This lets you build APIs that enforce naming conventions at compile time — catching case errors before runtime.

Built-in Intrinsic String Types

// TypeScript 4.1+ built-in intrinsic string manipulation types
type Upper = Uppercase<'hello world'>;   // "HELLO WORLD"
type Lower = Lowercase<'HELLO WORLD'>;   // "hello world"
type Cap = Capitalize<'hello world'>;    // "Hello world"
type Uncap = Uncapitalize<'Hello'>;      // "hello"

// Practical use: type-safe event names
type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent = EventName<'click'>;    // "onClick"
type ChangeEvent = EventName<'change'>;  // "onChange"
type FocusEvent = EventName<'focus'>;    // "onFocus"

// Build a typed event handler map
type EventHandlers<Events extends string> = {
  [E in Events as EventName<E>]?: (e: Event) => void;
};

type MyHandlers = EventHandlers<'click' | 'focus' | 'blur'>;
// { onClick?: (e: Event) => void; onFocus?: ...; onBlur?: ...; }

// CSS property transformation: camelCase JS -> kebab-case CSS property
// TypeScript can enforce this at the type level
function cssVar<T extends string>(name: T): `var(--${T})` {
  return `var(--${name})` as `var(--${T})`;
}
const color = cssVar('primary-color');  // type: "var(--primary-color)"
// cssVar('primaryColor') is valid too, but the type reflects what you pass

Advanced Template Literal Types — CamelCase and KebabCase

// Type-level snake_case to camelCase conversion
// Works with TypeScript's type inference for simple cases

// KebabCase type: converts snake_case to kebab-case
type KebabCase<S extends string> =
  S extends `${infer Head}_${infer Tail}`
    ? `${Head}-${KebabCase<Tail>}`
    : S;

type Test1 = KebabCase<'hello_world_foo'>;  // "hello-world-foo"
type Test2 = KebabCase<'user_name'>;        // "user-name"
type Test3 = KebabCase<'my_api_key'>;       // "my-api-key"

// SnakeCase type: converts camelCase to snake_case using conditional types
// (Simplified version — TypeScript can't do regex at type level)
// For full camelCase -> snake_case, you need recursive types with infer

// CamelCase type: converts snake_case to camelCase
type CamelCase<S extends string> =
  S extends `${infer Head}_${infer Tail}`
    ? `${Head}${Capitalize<CamelCase<Tail>>}`
    : S;

type Test4 = CamelCase<'hello_world'>;      // "helloWorld"
type Test5 = CamelCase<'user_api_key'>;     // "userApiKey"
type Test6 = CamelCase<'get_http_response'>; // "getHttpResponse"

// Practical: type-safe object key transformer
type CamelizeKeys<T extends Record<string, unknown>> = {
  [K in keyof T as CamelCase<K & string>]: T[K] extends Record<string, unknown>
    ? CamelizeKeys<T[K]>
    : T[K];
};

type ApiResponse = {
  user_name: string;
  email_address: string;
  profile_data: {
    theme_color: string;
    font_size: number;
  };
};

type Normalized = CamelizeKeys<ApiResponse>;
// {
//   userName: string;
//   emailAddress: string;
//   profileData: { themeColor: string; fontSize: number; }
// }

// The type checker now enforces correct key names
function normalize(data: ApiResponse): Normalized {
  return {
    userName: data.user_name,
    emailAddress: data.email_address,
    profileData: {
      themeColor: data.profile_data.theme_color,
      fontSize: data.profile_data.font_size,
    }
  };
}

Type-Safe Configuration Keys

// Enforce env var naming convention at type level
type EnvVarName = `${Uppercase<string>}_${Uppercase<string>}`;

// Type-safe CSS custom property access
type CssVariable<T extends string> = `--${KebabCase<T>}`;

type ButtonVars = CssVariable<'primary_color' | 'border_radius' | 'font_size'>;
// "--primary-color" | "--border-radius" | "--font-size"

// Typed path case for file system operations
type PathCase<S extends string> = KebabCase<Lowercase<S>>;

type ComponentPath<Name extends string> = `/components/${PathCase<Name>}/index.tsx`;

type ButtonPath = ComponentPath<'NavBar'>;      // "/components/nav-bar/index.tsx"
type CardPath = ComponentPath<'ProductCard'>;   // "/components/product-card/index.tsx"

// Zod schema with type-safe key validation
import { z } from 'zod';

// Validate that all keys in a submitted form are camelCase
const camelCaseKey = z.string().regex(/^[a-z][a-zA-Z0-9]*$/, 'Must be camelCase');

const dynamicFormSchema = z.record(camelCaseKey, z.unknown());
// Accepts: { userName: 'alice', emailAddress: 'a@b.com' }
// Rejects: { user_name: 'alice' } -- TS/Zod will catch this

Key Takeaways

  • Use camelCase for JS/TS variables and functions, PascalCase for classes and React components.
  • snake_case is required for Python (PEP 8), PostgreSQL columns, and Rust; kebab-case for CSS, HTML, and URLs.
  • Use SCREAMING_SNAKE_CASE universally for constants and environment variables across all languages.
  • The change-case npm library handles all JS/TS case conversion with correct acronym and unicode support.
  • The humps Python library (pip install pyhumps) provides recursive dict key conversion for API payloads.
  • Use Axios interceptors or fetch middleware to automatically convert API keys between snake_case and camelCase.
  • ORMs like Prisma and SQLAlchemy handle the snake_case database to camelCase application mapping automatically.
  • URL slugs require unicode normalization (NFD decomposition) to correctly handle accented characters before slugifying.
  • TypeScript template literal types (Uppercase<T>, Capitalize<T>) enable compile-time case enforcement for API contracts.
  • File naming case matters on case-sensitive Linux servers — always use consistent PascalCase for React components and snake_case for Python modules.
𝕏 Twitterin LinkedIn
Was this helpful?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Try These Related Tools

AaString Case ConverterAaString Case ConverterAaText Case Converter🔗URL Slug Generator

Related Articles

Hash Generator Online — MD5, SHA-256, SHA-512: The Complete Developer Guide

Free hash generator online supporting MD5, SHA-1, SHA-256, and SHA-512. Learn how hash algorithms work, compare MD5 vs SHA-256 vs SHA-512, password hashing with bcrypt/Argon2, HMAC, blockchain hashing, and code examples in JavaScript, Python, and Go.

Regex Tester Online: Test, Debug & Validate Regular Expressions (2026 Guide)

Use our free online regex tester to test regular expressions in real time. Covers JavaScript, Python, and Go regex syntax, 10 must-know patterns, common mistakes, performance tips, and when to skip regex.

JWT Decoder Online: Decode, Inspect & Debug JSON Web Tokens (2026 Guide)

Use our free JWT decoder online to instantly inspect JWT headers, payloads, and claims. Covers JWT structure, standard claims, decoding in JavaScript, Python, Go, Java, signing algorithms, security best practices, and common mistakes.

URL Encoder Decoder Online Guide: Percent Encoding, RFC 3986, and Best Practices

Complete guide to URL encoding (percent encoding). Covers RFC 3986 reserved and unreserved characters, encodeURIComponent vs encodeURI, urllib.parse.quote, URLEncoder in Java, common encoded characters, form encoding, API query parameters, double encoding debugging, and URL-safe Base64.