Skip to main content
Search...

Chapter 6: Message Keys

Add typed i18n message registries using @duckgen JSDoc tags.

What are message keys?

Most APIs return status messages alongside data. For example, a login endpoint might return "LOGIN_SUCCESS" or "INVALID_CREDENTIALS". These message keys are often used for i18n (internationalization), where the client maps keys to localized strings.

Without Duck Gen, these keys are plain strings. A typo in either the server or the client goes unnoticed until runtime. Duck Gen solves this by generating typed message registries.

How it works

Duck Gen scans for exported variables that have a @duckgen JSDoc tag. The tag tells Duck Gen the group name for those messages.

Step 1: Define message constants

Create a messages file in your Users module:

src/users/users.messages.ts
/**
 * @duckgen messages users
 */
export const UserMessages = {
  USER_CREATED: 'User created successfully',
  USER_UPDATED: 'User updated successfully',
  USER_DELETED: 'User deleted successfully',
  USER_FOUND: 'User found',
  USERS_LISTED: 'Users listed',
  USER_NOT_FOUND: 'User not found',
  USER_EMAIL_TAKEN: 'Email is already in use',
} as const

Key parts:

  • The @duckgen messages users JSDoc tag tells Duck Gen to scan this constant and group it under users.
  • The as const assertion is required so TypeScript treats the keys as literal types, not just string.
  • The object keys (USER_CREATED, USER_UPDATED, etc.) become the typed message keys.
  • The values ('User created successfully', etc.) can be anything. Duck Gen only reads the keys.

Step 2: Use messages in your controller

Update the Users controller to use these message constants:

src/users/users.controller.ts
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'
import { ApiResponse } from '../common/api-response'
import { CreateUserDto } from './dto/create-user.dto'
import { PaginationDto } from './dto/pagination.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { UserDto } from './dto/user.dto'
import { UserMessages } from './users.messages'
 
type UserMessage = keyof typeof UserMessages
 
@Controller('users')
export class UsersController {
  @Get()
  findAll(@Query() query: PaginationDto): Promise<ApiResponse<UserDto[], UserMessage>> {
    return {
      data: [],
      message: 'USERS_LISTED',
      success: true,
    } as any
  }
 
  @Post()
  create(@Body() body: CreateUserDto): Promise<ApiResponse<UserDto, UserMessage>> {
    return {
      data: {} as UserDto,
      message: 'USER_CREATED',
      success: true,
    } as any
  }
 
  // ... other methods
}

Step 3: Regenerate types

bun run generate

Step 4: Check the output

Open generated/duck-gen-messages.d.ts. You will see types like:

generated/duck-gen-messages.d.ts
export interface DuckgenMessagesByGroup {
  users: {
    USER_CREATED: 'User created successfully'
    USER_UPDATED: 'User updated successfully'
    USER_DELETED: 'User deleted successfully'
    USER_FOUND: 'User found'
    USERS_LISTED: 'Users listed'
    USER_NOT_FOUND: 'User not found'
    USER_EMAIL_TAKEN: 'Email is already in use'
  }
}
 
export type DuckgenMessageKeys = keyof DuckgenMessagesByGroup[keyof DuckgenMessagesByGroup]

Using message types on the client

Now you can build a type-safe i18n system:

client/i18n/messages.ts
import type { DuckgenMessagesByGroup } from '@gentleduck/gen/nestjs'
 
// Define translations for each language
const translations: Record<string, Record<string, string>> = {
  en: {
    USER_CREATED: 'User created successfully',
    USER_NOT_FOUND: 'User not found',
    USER_EMAIL_TAKEN: 'This email is already taken',
    // ...
  },
  ar: {
    USER_CREATED: 'User created in Arabic',
    USER_NOT_FOUND: 'User not found in Arabic',
    USER_EMAIL_TAKEN: 'Email taken in Arabic',
    // ...
  },
}
 
// Type-safe message lookup
type UserMessageKey = keyof DuckgenMessagesByGroup['users']
 
function getMessage(lang: string, key: UserMessageKey): string {
  return translations[lang]?.[key] || key
}
 
// Usage
getMessage('en', 'USER_CREATED')      // works
getMessage('en', 'INVALID_KEY')        // TypeScript error

Multiple message groups

You can have multiple message groups across different modules:

src/auth/auth.messages.ts
/**
 * @duckgen messages auth
 */
export const AuthMessages = {
  LOGIN_SUCCESS: 'Logged in successfully',
  LOGIN_FAILED: 'Invalid credentials',
  TOKEN_EXPIRED: 'Session expired',
  LOGOUT_SUCCESS: 'Logged out successfully',
} as const

Each group appears as a separate key in DuckgenMessagesByGroup:

type Groups = keyof DuckgenMessagesByGroup // 'users' | 'auth'

Tag format

The @duckgen tag follows this format:

@duckgen messages <group-name>
  • messages is the scan type (currently the only supported type for messages).
  • <group-name> is the key under which these messages are grouped.

Rules:

  • Each group name must be unique across your entire project.
  • The tagged variable must be exported.
  • Use as const so TypeScript preserves literal key types.

Next

Chapter 7: Duck Query Client