読み込み中
豆知識
フロント・バック・DB で揃える命名の三層ルール(DB は snake_case、API は camelCase、URL は kebab-case)です。
Our naming convention follows a three-layer architecture that aligns with industry standards:
Frontend (camelCase everywhere) → Middleware Conversion → Backend (snake_case) → Database (snake_case)
Request Bodies: camelCase → converted → snake_case
Query Params: camelCase → converted → snake_case
Path Params: snake_case (no conversion - part of URL routing)
Response Bodies: snake_case → converted → camelCase
| Layer | Convention | Purpose | Example |
|---|---|---|---|
| Database | snake_case |
SQL/PostgreSQL standard | user_profiles, created_at, is_deleted |
| API Contract | camelCase |
JavaScript/JSON standard | userId, createdAt, isDeleted |
| Validation DTOs | snake_case |
Database alignment | is_locked, document_name, page_size |
| TypeScript Code | camelCase |
JavaScript standard | getUserProfile(), documentService |
| URLs | kebab-case |
REST API standard | /api/user-profiles, /api/bank-statements |
All database entities use snake_case following PostgreSQL and SQL conventions.
-- ✅ Correct
user_profiles
bank_statements
journal_entries
expense_reports
-- ❌ Incorrect
userProfiles
bankStatements
journalEntries
-- ✅ Correct
CREATE TABLE user_profiles (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
display_name VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
is_deleted BOOLEAN DEFAULT FALSE,
deleted_at TIMESTAMPTZ
);
-- ❌ Incorrect
CREATE TABLE user_profiles (
id UUID PRIMARY KEY,
userId UUID NOT NULL, -- Should be user_id
displayName VARCHAR(255), -- Should be display_name
createdAt TIMESTAMPTZ, -- Should be created_at
isDeleted BOOLEAN -- Should be is_deleted
);
// ✅ Correct - matches database exactly
model user_profiles {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
user_id String @unique @db.Uuid
display_name String?
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
is_deleted Boolean @default(false)
deleted_at DateTime? @db.Timestamptz(6)
}
API requests and responses follow different conventions depending on the parameter type.
// ✅ Frontend sends (camelCase) - automatically converted to snake_case
POST /api/documents
{
"documentName": "給与明細書",
"isLocked": false,
"createdAt": "2026-01-09T10:00:00Z"
}
// ✅ Snake_case still works for body
{
"document_name": "給与明細書",
"is_locked": false,
"created_at": "2026-01-09T10:00:00Z"
}
// ✅ Backend responds (camelCase) - automatically converted from snake_case
{
"success": true,
"data": {
"documentId": "123e4567-e89b-12d3-a456-426614174000",
"documentName": "給与明細書",
"isLocked": false,
"createdAt": "2026-01-09T10:00:00Z",
"updatedAt": "2026-01-09T10:00:00Z"
}
}
// ✅ Frontend can use camelCase for query parameters (automatically converted)
GET /api/clients?clientType=1&pageSize=20&pageNumber=1&isQualified=true
// ✅ Backend receives snake_case after middleware conversion
// req.query = { client_type: '1', page_size: '20', page_number: '1', is_qualified: 'true' }
// req.validatedQuery = { client_type: 1, page_size: 20, page_number: 1, is_qualified: true }
// ✅ Frontend can also use snake_case directly (no conversion needed)
GET /api/clients?client_type=1&page_size=20&page_number=1&is_qualified=true
// ✅ Frontend must use snake_case for path parameters
GET /api/clients/123e4567-e89b-12d3-a456-426614174000
DELETE /api/documents/456e7890-e89b-12d3-a456-426614174000
// Path parameter validation expects snake_case
// req.params = { client_id: '123e4567-e89b-12d3-a456-426614174000' }
// req.validatedParams = { client_id: '123e4567-e89b-12d3-a456-426614174000' }
All validation schemas use snake_case to align with database conventions and maintain consistency.
// ✅ Correct - snake_case for validation schemas
export class LockDocumentBody {
@IsBoolean({ message: "is_locked must be a boolean value" })
@IsDefined({ message: "is_locked is required" })
is_locked!: boolean
}
export class DocumentQuery {
@IsOptional()
@Transform(({ value }) => parseInt(String(value), 10))
@IsNumber()
@Min(1)
page_size: number = 20
@IsOptional()
@Transform(({ value }) => value === "true" || value === true)
include_deleted?: boolean
}
// ❌ Legacy pattern (being migrated away from)
export class LegacyDocumentBody {
@IsBoolean()
isLocked!: boolean // Should be is_locked
@IsString()
documentName!: string // Should be document_name
}
// ✅ Use req.validatedBody for type-safe snake_case access
const { is_locked, document_name } = req.validatedBody
// ✅ Direct use in database operations (snake_case)
await prisma.documents.update({
where: { id: documentId },
data: {
is_locked: req.validatedBody.is_locked, // Consistent snake_case
document_name: req.validatedBody.document_name,
},
})
All TypeScript code follows JavaScript conventions.
// ✅ Correct
const documentService = DocumentService.getInstance()
const userId = context.userId
const createdAt = new Date()
async function createDraftDocument(dto: CreateDocument) {
// Implementation
}
// ❌ Incorrect
const document_service = DocumentService.getInstance() // Should be documentService
const user_id = context.userId // Should be userId
const created_at = new Date() // Should be createdAt
// ✅ Correct
export class DocumentService {
private readonly documentExpenseService: DocumentExpenseService
async createDraftDocument(params: CreateDocumentParams): Promise<Document> {
// Implementation
}
async generateDocumentContent(dto: GenerateDocumentContent): Promise<any> {
// Implementation
}
}
// ❌ Incorrect
export class document_service {
// Should be DocumentService (PascalCase)
async create_draft_document() {
// Should be createDraftDocument
// Implementation
}
}
// ✅ Correct - Constants use UPPER_SNAKE_CASE
const MAX_FILE_SIZE = 50 * 1024 * 1024
const DEFAULT_PAGE_SIZE = 20
const API_TIMEOUT_MS = 30000
// ✅ Correct - Enum keys use UPPER_SNAKE_CASE, values use snake_case
export enum DocumentType {
SALES_QUOTATION = "sales_quotation",
SALES_INVOICE = "sales_invoice",
SERVICE_AGREEMENT = "service_agreement",
COMPANY_INFO = "company_info",
}
// ❌ Incorrect
const maxFileSize = 50 * 1024 * 1024 // Should be MAX_FILE_SIZE
export enum DocumentType {
salesQuotation = "sales_quotation", // Should be SALES_QUOTATION
SALES_INVOICE = "salesInvoice", // Value should be 'sales_invoice'
}
API endpoints use kebab-case following REST API standards.
// ✅ Correct
;/api/cdemnostu / api / bank -
statements / api / user -
profiles / api / expense -
reports / api / journal -
entries / api / client -
usage -
tracking /
// ❌ Incorrect
api /
bankStatements / // Should be /api/bank-statements
api /
user_profiles / // Should be /api/user-profiles
api /
expenseReports // Should be /api/expense-reports
// ✅ Correct - Parameters use snake_case
router.get("/:document_id", controller.getDocument)
router.post("/:journal_entry_id/post", controller.postJournalEntry)
router.delete("/:expense_report_id", controller.deleteExpenseReport)
// ❌ Incorrect
router.get("/:documentId", controller.getDocument) // Should be :document_id
router.post("/:journalEntryId/post", controller.post) // Should be :journal_entry_id
snake_case for Database?snake_caseSELECT user_id, created_at FROM user_profiles is more readablesnake_case predictablecamelCase for API Bodies?camelCasecamelCasecamelCase, enabling better type checkingcamelCasesnake_case to match database conventionssnake_case, including query parameterssnake_case for Validation?snake_case to match database exactlyOur middleware handles case conversion for request bodies AND query parameters:
Frontend Request Body (camelCase)
↓ case-conversion.middleware.ts
Backend Request Body (snake_case in req.body)
↓ validate-schema.middleware.ts
Validation (snake_case in req.validatedBody)
↓ Controllers & Services
Database Operations (snake_case)
Frontend Query Parameters (camelCase)
↓ case-conversion.middleware.ts
Backend Query Params (snake_case in req.query)
↓ validate-schema.middleware.ts
Validation (snake_case in req.validatedQuery)
↓ Controllers & Services
Database Operations (snake_case)
Frontend Path Params (snake_case in URL)
↓ NO conversion (part of URL routing)
Backend Path Params (snake_case in req.params)
↓ validate-schema.middleware.ts
Validation (snake_case in req.validatedParams)
↓ Controllers & Services
Database Operations (snake_case)
Database Response (snake_case)
↓ case-conversion.middleware.ts
API Response (camelCase)
↓ Network
Frontend Receives (camelCase)
// Applied globally in app configuration
app.use(requestCaseConversionMiddleware) // Converts camelCase → snake_case
app.use(responseCaseConversionMiddleware) // Converts snake_case → camelCase
This codebase is transitioning from mixed conventions to the standardized three-layer approach.
// ❌ Legacy - camelCase in validation schemas
export class OldDocumentSchema {
@IsString()
documentName!: string // Should be document_name
@IsBoolean()
isLocked!: boolean // Should be is_locked
}
// ✅ New - snake_case in validation schemas
export class NewDocumentSchema {
@IsString()
document_name!: string
@IsBoolean()
is_locked!: boolean
}
snake_casereq.validatedBody for consistent snake_case access in controllerssnake_casecamelCase (middleware handles conversion)// ✅ Frontend can use camelCase everywhere (body AND query params)
const response = await fetch(
"/api/documents/draft?documentType=salary_slip&pageSize=20",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
documentName: "給与明細書", // camelCase in body (will be converted)
isLocked: false,
}),
}
)
// ✅ Query parameters - both camelCase and snake_case work
const clientsResponse = await fetch(
"/api/clients?clientType=1&pageSize=20&isQualified=true"
) // camelCase (converted)
const clientsResponse2 = await fetch(
"/api/clients?client_type=1&page_size=20&is_qualified=true"
) // snake_case (direct)
// ✅ Body schema uses snake_case (receives converted camelCase)
export class CreateDocument {
@IsString()
@IsNotEmpty()
document_name!: string
@IsBoolean()
@IsOptional()
is_locked?: boolean
}
// ✅ Query schema uses snake_case (receives snake_case directly)
export class DocumentQuery extends PaginationQuery {
@IsString()
@IsOptional()
document_type?: string
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@IsOptional()
page_size?: number = 20
@Transform(({ value }) => value === "true" || value === true)
@IsBoolean()
@IsOptional()
is_qualified?: boolean
}
// ✅ Path parameter schema uses snake_case
export class DocumentIdParams {
@IsString()
@IsNotEmpty()
document_id!: string
}
// ✅ Controller uses snake_case consistently for all validated data
export class DocumentController {
async createDraftDocument(req: Request, res: Response) {
// Use validatedBody for body parameters (converted from camelCase)
const { document_name, is_locked } = req.validatedBody
// Use validatedQuery for query parameters (received as snake_case)
const { document_type, page_size } = req.validatedQuery
// Use validatedParams for path parameters (received as snake_case)
const { document_id } = req.validatedParams
// Service layer uses snake_case directly for database
const document = await this.documentService.createDraftDocument({
document_name, // Already in snake_case from validation
document_type,
is_locked,
})
// Response automatically converted to camelCase by middleware
return { success: true, data: document }
}
}
// ✅ Service uses snake_case for database operations
export class DocumentService {
async createDraftDocument(params: {
document_name: string
document_type: string
}) {
return await prisma.documents.create({
data: {
document_name: params.document_name, // Database uses snake_case
document_type: params.document_type,
user_id: userId,
created_at: new Date(),
updated_at: new Date(),
},
})
}
}
-- ✅ Database uses snake_case
INSERT INTO documents (
document_name,
document_type,
user_id,
created_at,
updated_at
) VALUES (
'給与明細書',
'salary_slip',
'123e4567-e89b-12d3-a456-426614174000',
NOW(),
NOW()
);
// ✅ Frontend receives camelCase (auto-converted)
{
"success": true,
"data": {
"documentId": "123e4567-e89b-12d3-a456-426614174000",
"documentName": "給与明細書",
"documentType": "salary_slip",
"userId": "456e7890-e89b-12d3-a456-426614174000",
"createdAt": "2026-01-09T10:00:00Z",
"updatedAt": "2026-01-09T10:00:00Z"
}
}
// ❌ Bad - mixing snake_case and camelCase in validation
export class BadSchema {
@IsString()
document_name!: string // snake_case
@IsBoolean()
isLocked!: boolean // camelCase - inconsistent!
}
// ✅ Good - consistent snake_case
export class GoodSchema {
@IsString()
document_name!: string
@IsBoolean()
is_locked!: boolean
}
// ❌ Bad - frontend using snake_case for request body
const requestBody = {
document_name: "給与明細書", // Should be documentName
is_locked: false, // Should be isLocked
}
// ✅ Good - frontend using camelCase for request body
const requestBody = {
documentName: "給与明細書",
isLocked: false,
}
// ✅ Good - frontend using camelCase for query parameters (converted by middleware)
fetch("/api/clients?clientType=1&pageSize=20&isQualified=true")
// ✅ Good - frontend using snake_case for query parameters (no conversion needed)
fetch("/api/clients?client_type=1&page_size=20&is_qualified=true")
// ⚠️ Path parameters must use snake_case (no conversion)
fetch("/api/clients/:client_id/stats") // ✅ Correct
fetch("/api/clients/:clientId/stats") // ❌ Won't work - path params are not converted
-- ❌ Bad - database using camelCase
CREATE TABLE documents (
documentId UUID PRIMARY KEY, -- Should be document_id
documentName VARCHAR(255), -- Should be document_name
isLocked BOOLEAN DEFAULT FALSE -- Should be is_locked
);
-- ✅ Good - database using snake_case
CREATE TABLE documents (
document_id UUID PRIMARY KEY,
document_name VARCHAR(255),
is_locked BOOLEAN DEFAULT FALSE
);
// ❌ Bad - accessing raw body instead of validated body
const isLocked = req.body.is_locked // Use req.validatedBody.is_locked instead
// ❌ Bad - manual response conversion
return {
success: true,
data: {
documentId: result.document_id, // Middleware handles this
isLocked: result.is_locked,
},
}
// ✅ Good - use validated data and let middleware handle conversion
const { is_locked } = req.validatedBody
const { page_size } = req.validatedQuery
const { document_id } = req.validatedParams
return { success: true, data: result } // Middleware converts automatically
// ✅ Good - query params ARE converted like body params
// Frontend sends: /api/clients?pageSize=20&clientType=1
// Middleware converts to: { page_size: '20', client_type: '1' }
// Validation schemas expect snake_case ✓
// ✅ Also Good - snake_case is passed through directly
// Frontend sends: /api/clients?page_size=20&client_type=1
// No conversion needed, validation works directly ✓
| Context | Convention | Example |
|---|---|---|
| Database Tables | snake_case |
user_profiles, bank_statements |
| Database Columns | snake_case |
user_id, created_at, is_deleted |
| API URLs | kebab-case |
/api/user-profiles, /api/bank-statements |
| Route Parameters | snake_case |
:document_id, :user_id |
| Request/Response JSON | camelCase |
userId, createdAt, isDeleted |
| Query Parameters | camelCase (converted) or snake_case |
pageSize=20 or page_size=20 |
| Validation Schemas | snake_case |
document_name, is_locked, page_size |
| TypeScript Variables | camelCase |
documentService, userId, createdAt |
| TypeScript Functions | camelCase |
createDocument(), getUserProfile() |
| TypeScript Classes | PascalCase |
DocumentService, UserController |
| Constants | UPPER_SNAKE_CASE |
MAX_FILE_SIZE, API_TIMEOUT_MS |
| Enum Keys | UPPER_SNAKE_CASE |
SALES_INVOICE, COMPANY_INFO |
| Enum Values | snake_case |
'sales_invoice', 'company_info' |
These naming conventions ensure:
When in doubt, follow the layer-appropriate convention and let the middleware handle conversions automatically.
最終更新: January 12, 2026 Version: 4.7 現在の位置づけ: Active Standard (Query param conversion enabled)
読み込み中
豆知識