NIK
Utilities for working with Indonesian National Identity Number (Nomor Induk Kependudukan).
Overview
The NIK module provides type-safe utilities to validate, parse, format, and mask Indonesian national identity numbers. Built with zero dependencies, fully tested, and optimized for both browser and Node.js environments.
What is NIK?
NIK (Nomor Induk Kependudukan) is Indonesiaโs unique 16-digit national identity number issued to all Indonesian citizens. Each NIK contains encoded information about:
- Location: Province, regency (kabupaten/kota), and district (kecamatan)
- Birth Date: Year, month, and day of birth
- Gender: Encoded in the day digit (+40 for females)
- Unique ID: Serial number for people born on the same date and location
NIK Structure
A valid NIK consists of 16 digits organized as follows:
3201018901310123
โโโโโโโโโโโโโโดโดโดโ Serial Number (4 digits)
โโโโโโโโโโดโดโโโโโโโ Day (01-31 for male, 41-71 for female)
โโโโโโโโดโโโโโโโโโโ Month (01-12)
โโโโโโดโโโโโโโโโโโโ Year (last 2 digits)
โโโโดโโโโโโโโโโโโโโ District code (2 digits)
โโดโโโโโโโโโโโโโโโโ Regency code (2 digits)
โโโโโโโโโโโโโโโโโโ Province code (2 digits)Breaking down the example 3201018901310123:
| Position | Value | Meaning |
|---|---|---|
| 1-2 | 32 | Province: Jawa Barat |
| 3-4 | 01 | Regency: Kab. Bogor |
| 5-6 | 01 | District code |
| 7-8 | 89 | Year: 1989 |
| 9-10 | 01 | Month: January |
| 11-12 | 31 | Day: 31st (male) |
| 13-16 | 0123 | Serial number |
Gender Encoding: For females, the day is encoded as actual_day + 40. For example, a female born on January 15th would have 55 (15 + 40) in the day position.
Installation
npm
npm install @indodev/toolkitQuick Start
import { validateNIK, parseNIK, formatNIK, maskNIK } from '@indodev/toolkit/nik'
// Validate a NIK
const isValid = validateNIK('3201018901310123')
console.log(isValid) // true
// Extract information from NIK
const info = parseNIK('3201018901310123')
console.log(info?.province.name) // 'Jawa Barat'
console.log(info?.gender) // 'male'
console.log(info?.birthDate) // Date object: 1989-01-31
// Format for display
const formatted = formatNIK('3201018901310123')
console.log(formatted) // '32-01-01-89-01-31-0123'
// Mask for privacy
const masked = maskNIK('3201018901310123')
console.log(masked) // '3201********0123'API Reference
validateNIK()
Validates whether a string is a valid NIK format.
Type Signature:
function validateNIK(nik: string): booleanParameters:
| Name | Type | Description |
|---|---|---|
nik | string | The NIK string to validate |
Returns:
boolean - Returns true if the NIK is valid, false otherwise.
Validation Rules:
- Must be exactly 16 digits
- Province code must be valid (from official Dukcapil data)
- Date components must form a valid calendar date
- Day must be 1-31 for males or 41-71 for females
Examples:
// Valid NIK
validateNIK('3201018901310123')
// โ
true
// Invalid: wrong length
validateNIK('1234')
// โ false
// Invalid: non-numeric characters
validateNIK('320101890131012A')
// โ false
// Invalid: province code doesn't exist
validateNIK('9912345678901234')
// โ false
// Invalid: impossible date (February 30th)
validateNIK('3201018902300123')
// โ falseUse Cases:
// Form validation
function handleSubmit(formData: FormData) {
const nik = formData.get('nik') as string
if (!validateNIK(nik)) {
return { error: 'Invalid NIK format' }
}
// Process valid NIK
return { success: true }
}
// Batch validation
const niks = ['3201018901310123', '3175031234567890', 'invalid']
const validNiks = niks.filter(validateNIK)
console.log(validNiks) // ['3201018901310123', '3175031234567890']parseNIK()
Parses a NIK string and extracts all embedded information including location, birth date, and gender.
Type Signature:
function parseNIK(nik: string): NIKInfo | nullParameters:
| Name | Type | Description |
|---|---|---|
nik | string | The 16-digit NIK string to parse |
Returns:
NIKInfo | null - Returns a NIKInfo object if parsing succeeds, or null if the NIK is invalid.
Examples:
// Parse male NIK
const male = parseNIK('3201018901310123')
console.log(male)
// {
// province: { code: '32', name: 'Jawa Barat' },
// regency: { code: '01', name: 'Kab. Bogor' },
// district: { code: '01', name: null },
// birthDate: Date('1989-01-31'),
// gender: 'male',
// serialNumber: '0123',
// isValid: true
// }
// Parse female NIK (day + 40)
const female = parseNIK('3201019508550123')
console.log(female?.gender) // 'female'
console.log(female?.birthDate) // Date('1995-08-15')
// Day 55 = 15 + 40 (female encoding)
// Parse Jakarta NIK
const jakarta = parseNIK('3175031234567890')
console.log(jakarta?.province.name) // 'DKI Jakarta'
console.log(jakarta?.regency.name) // 'Kota Jakarta Pusat'
// Invalid NIK returns null
const invalid = parseNIK('invalid-nik')
console.log(invalid) // nullUse Cases:
// Extract age from NIK
function getAgeFromNIK(nik: string): number | null {
const info = parseNIK(nik)
if (!info?.birthDate) return null
const today = new Date()
const birthDate = info.birthDate
let age = today.getFullYear() - birthDate.getFullYear()
// Adjust if birthday hasn't occurred this year
const monthDiff = today.getMonth() - birthDate.getMonth()
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--
}
return age
}
// Display user information
function displayUserInfo(nik: string) {
const info = parseNIK(nik)
if (!info) {
return 'Invalid NIK'
}
return `
Location: ${info.province.name}, ${info.regency.name}
Birth Date: ${info.birthDate?.toLocaleDateString('id-ID')}
Gender: ${info.gender === 'male' ? 'Laki-laki' : 'Perempuan'}
`
}
// Filter by province
function filterByProvince(niks: string[], provinceCode: string) {
return niks.filter(nik => {
const info = parseNIK(nik)
return info?.province.code === provinceCode
})
}Note on District Names: District names are currently null in the returned object as comprehensive district data is not yet included in the library. Province and regency data are complete and accurate.
formatNIK()
Formats a NIK string with separators for better readability.
Type Signature:
function formatNIK(nik: string, separator?: string): stringParameters:
| Name | Type | Default | Description |
|---|---|---|---|
nik | string | - | The 16-digit NIK string to format |
separator | string | '-' | Character to use as separator between segments |
Returns:
string - Formatted NIK string with separators, or the original string if invalid.
Format Pattern:
PP-KK-DD-YY-MM-DD-SSSS
PP = Province code
KK = Regency code
DD = District code
YY = Birth year (2 digits)
MM = Birth month
DD = Birth day (add 40 for female)
SSSS = Serial numberExamples:
// Default separator (dash)
formatNIK('3201018901310123')
// '32-01-01-89-01-31-0123'
// Custom separator: space
formatNIK('3201018901310123', ' ')
// '32 01 01 89 01 31 0123'
// Custom separator: dot
formatNIK('3201018901310123', '.')
// '32.01.01.89.01.31.0123'
// Custom separator: underscore
formatNIK('3201018901310123', '_')
// '32_01_01_89_01_31_0123'
// Invalid NIK returns as-is
formatNIK('1234')
// '1234'
// Empty separator (no formatting)
formatNIK('3201018901310123', '')
// '3201018901310123'Use Cases:
// Display in UI
function NIKDisplay({ nik }: { nik: string }) {
return <div>NIK: {formatNIK(nik)}</div>
}
// Custom formatting for different contexts
function formatForDocument(nik: string) {
return formatNIK(nik, ' ') // Space-separated for documents
}
function formatForDatabase(nik: string) {
return formatNIK(nik, '') // No separator for storage
}
// Format list of NIKs
const niks = ['3201018901310123', '3175031234567890']
const formatted = niks.map(nik => formatNIK(nik))
console.log(formatted)
// ['32-01-01-89-01-31-0123', '31-75-03-12-34-56-7890']maskNIK()
Masks a NIK to protect privacy while maintaining partial visibility for verification purposes.
Type Signature:
function maskNIK(nik: string, options?: MaskOptions): stringParameters:
| Name | Type | Description |
|---|---|---|
nik | string | The 16-digit NIK string to mask |
options | MaskOptions | Optional configuration for masking behavior |
Returns:
string - Masked NIK string, or the original string if invalid.
Options:
interface MaskOptions {
start?: number // Characters to show at start (default: 4)
end?: number // Characters to show at end (default: 4)
char?: string // Mask character (default: '*')
separator?: string // Optional separator (default: none)
}Examples:
// Default masking (first 4, last 4)
maskNIK('3201018901310123')
// '3201********0123'
// Custom visible characters
maskNIK('3201018901310123', { start: 6, end: 4 })
// '320101******0123'
maskNIK('3201018901310123', { start: 2, end: 2 })
// '32************23'
// Custom mask character
maskNIK('3201018901310123', { char: 'X' })
// '3201XXXXXXXX0123'
maskNIK('3201018901310123', { char: 'โข' })
// '3201โขโขโขโขโขโขโขโข0123'
// With separator
maskNIK('3201018901310123', { separator: '-' })
// '32-01-**-**-**-**-0123'
maskNIK('3201018901310123', { separator: ' ' })
// '32 01 ** ** ** ** 0123'
// Combined options
maskNIK('3201018901310123', {
start: 6,
end: 4,
char: 'X',
separator: '-'
})
// '32-01-01-XX-XX-XX-0123'
// Complete masking
maskNIK('3201018901310123', { start: 0, end: 0 })
// '****************'
// Invalid NIK returns as-is
maskNIK('invalid')
// 'invalid'Use Cases:
// Display in public listings
function UserCard({ user }) {
return (
<div>
<h3>{user.name}</h3>
<p>NIK: {maskNIK(user.nik)}</p>
</div>
)
}
// Different masking levels based on permission
function displayNIK(nik: string, userRole: string) {
switch (userRole) {
case 'admin':
return formatNIK(nik) // Full visibility
case 'staff':
return maskNIK(nik, { start: 6, end: 4 }) // Partial
default:
return maskNIK(nik, { start: 4, end: 0 }) // Minimal
}
}
// Mask for logging
function logUserAction(nik: string, action: string) {
console.log(`User ${maskNIK(nik)} performed: ${action}`)
}
// Privacy-compliant export
function exportUserData(users: User[]) {
return users.map(user => ({
...user,
nik: maskNIK(user.nik, { separator: '-' })
}))
}Privacy Best Practice: When displaying NIKs in logs, public interfaces, or shared systems, always use masking to protect user privacy while maintaining enough information for verification.
Type Reference
NIKInfo
Information extracted from a valid NIK.
interface NIKInfo {
province: {
code: string // Two-digit province code (e.g., '32')
name: string // Full province name (e.g., 'Jawa Barat')
}
regency: {
code: string // Two-digit regency code (e.g., '01')
name: string // Full regency name (e.g., 'Kab. Bogor')
}
district: {
code: string // Two-digit district code (e.g., '01')
name: string | null // District name (currently null)
}
birthDate: Date | null // Parsed birth date
gender: 'male' | 'female' | null // Derived from day encoding
serialNumber: string | null // Four-digit serial number
isValid: boolean // Overall validity flag
}Property Details:
- province: Province information from first 2 digits
- regency: Regency (Kabupaten/Kota) from digits 3-4
- district: District (Kecamatan) from digits 5-6
- birthDate: JavaScript Date object from digits 7-12
- gender: Determined by day value (1-31 = male, 41-71 = female)
- serialNumber: Unique identifier from last 4 digits
- isValid:
trueif all validation checks passed
MaskOptions
Configuration options for masking NIKs.
interface MaskOptions {
start?: number // Characters to show at start (default: 4)
end?: number // Characters to show at end (default: 4)
char?: string // Mask character (default: '*')
separator?: string // Optional separator (default: none)
}Option Details:
- start: Number of characters visible at the beginning
- end: Number of characters visible at the end
- char: Character used for masking (single character)
- separator: If provided, formats the NIK with separators before masking
Constraint: start + end must be less than 16, otherwise the NIK is returned unmodified.
Common Use Cases
Age Verification
import { parseNIK } from '@indodev/toolkit/nik'
function isAdult(nik: string): boolean {
const info = parseNIK(nik)
if (!info?.birthDate) return false
const age = new Date().getFullYear() - info.birthDate.getFullYear()
return age >= 17 // Indonesian adult age
}
// Usage
if (isAdult(userNIK)) {
// Allow access
} else {
// Require parent consent
}Regional Filtering
import { parseNIK } from '@indodev/toolkit/nik'
// Find all users from Jakarta
function getUsersFromJakarta(niks: string[]) {
return niks.filter(nik => {
const info = parseNIK(nik)
return info?.province.code === '31' // DKI Jakarta
})
}
// Group by province
function groupByProvince(niks: string[]) {
return niks.reduce((acc, nik) => {
const info = parseNIK(nik)
if (!info) return acc
const province = info.province.name
if (!acc[province]) acc[province] = []
acc[province].push(nik)
return acc
}, {} as Record<string, string[]>)
}Form Validation
import { validateNIK, parseNIK } from '@indodev/toolkit/nik'
function validateUserForm(data: FormData) {
const nik = data.get('nik') as string
const birthDate = new Date(data.get('birthDate') as string)
// Validate NIK format
if (!validateNIK(nik)) {
return { error: 'NIK tidak valid' }
}
// Verify birth date matches NIK
const info = parseNIK(nik)
if (info?.birthDate?.toDateString() !== birthDate.toDateString()) {
return { error: 'Tanggal lahir tidak sesuai dengan NIK' }
}
return { success: true, data: info }
}Privacy-Safe Display
import { maskNIK, formatNIK } from '@indodev/toolkit/nik'
// Different views based on context
function NIKDisplay({ nik, context }: { nik: string, context: string }) {
switch (context) {
case 'admin':
return formatNIK(nik) // Full: '32-01-01-89-01-31-0123'
case 'profile':
return maskNIK(nik, { separator: '-' }) // Partial: '32-01-**-**-**-**-0123'
case 'public':
return maskNIK(nik, { start: 2, end: 2 }) // Minimal: '32************23'
default:
return '****-****-****-****' // Hidden
}
}Batch Processing
import { validateNIK, parseNIK } from '@indodev/toolkit/nik'
// Process CSV import
function processNIKBatch(niks: string[]) {
const results = {
valid: [] as string[],
invalid: [] as string[],
duplicates: [] as string[]
}
const seen = new Set<string>()
for (const nik of niks) {
if (!validateNIK(nik)) {
results.invalid.push(nik)
continue
}
if (seen.has(nik)) {
results.duplicates.push(nik)
continue
}
seen.add(nik)
results.valid.push(nik)
}
return results
}Best Practices
Input Sanitization
Always sanitize user input before validation:
function sanitizeNIK(input: string): string {
// Remove all non-numeric characters
return input.replace(/\D/g, '')
}
// Usage
const userInput = '32-01-01-89-01-31-0123'
const clean = sanitizeNIK(userInput) // '3201018901310123'
const isValid = validateNIK(clean)Error Handling
Handle parsing failures gracefully:
function safeParseNIK(nik: string): NIKInfo {
const info = parseNIK(nik)
if (!info) {
throw new Error('Invalid NIK format')
}
return info
}
// Usage with try-catch
try {
const info = safeParseNIK(userNIK)
console.log(info.province.name)
} catch (error) {
console.error('Failed to parse NIK:', error.message)
}Privacy Compliance
Always mask NIKs in logs and public displays:
// โ BAD: Exposing full NIK in logs
console.log(`User ${nik} logged in`)
// โ
GOOD: Masked NIK in logs
console.log(`User ${maskNIK(nik)} logged in`)
// โ
GOOD: Masked in public API responses
function getUserPublicProfile(nik: string) {
return {
nik: maskNIK(nik),
// other fields...
}
}Type Safety
Use TypeScript for type safety:
import type { NIKInfo, MaskOptions } from '@indodev/toolkit/nik'
// Type-safe function
function processNIK(nik: string): NIKInfo | null {
return parseNIK(nik)
}
// Type-safe options
const options: MaskOptions = {
start: 4,
end: 4,
char: '*',
separator: '-'
}Performance
For large datasets, validate once and cache results:
// Cache validation results
const validationCache = new Map<string, boolean>()
function cachedValidate(nik: string): boolean {
if (validationCache.has(nik)) {
return validationCache.get(nik)!
}
const isValid = validateNIK(nik)
validationCache.set(nik, isValid)
return isValid
}Troubleshooting
Common Issues
Q: Why does parseNIK() return null?
A: This can happen if:
- NIK length is not exactly 16 digits
- Province code is invalid
- Date components form an invalid date (e.g., February 30th)
// Check each component
const nik = '3201018902300123' // February 30th (invalid)
const info = parseNIK(nik)
console.log(info) // nullQ: Why is district.name always null?
A: Currently, the library only includes province and regency data. District data will be added in future versions.
Q: How do I handle female NIKs?
A: The library automatically handles the +40 encoding for females:
const female = parseNIK('3201019508550123')
console.log(female?.gender) // 'female'
console.log(female?.birthDate) // 1995-08-15 (day 55 - 40 = 15)Q: Can I use this with React/Vue/etc?
A: Yes! The library is framework-agnostic and works in any JavaScript environment:
// React example
import { useState } from 'react'
import { validateNIK, formatNIK } from '@indodev/toolkit/nik'
function NIKInput() {
const [nik, setNIK] = useState('')
const isValid = validateNIK(nik)
return (
<div>
<input
value={nik}
onChange={(e) => setNIK(e.target.value)}
className={isValid ? 'valid' : 'invalid'}
/>
{isValid && <p>Formatted: {formatNIK(nik)}</p>}
</div>
)
}Data Source
Province and regency codes are based on official data from:
- Dukcapil Kemendagri (Ministry of Home Affairs - Population and Civil Registration)
- Updated to include all 38 Indonesian provinces (including new Papua provinces)
For the most up-to-date administrative codes, always refer to official government sources.