Getting Started
Get up and running with @indodev/toolkit in minutes.
Installation
npm
npm install @indodev/toolkitUsage Pattern
The library uses subpath imports for optimal tree-shaking. This ensures your final bundle only includes the code you actually use.
// Recommended: Import from specific submodules
import { validateNIK } from '@indodev/toolkit/nik';
import { formatRupiah } from '@indodev/toolkit/currency';
// Supported but not recommended for large apps
import { validateNIK, formatRupiah } from '@indodev/toolkit';tsconfig.json has "moduleResolution": "bundler" (or "node16" / "nodenext"). See TypeScript Configuration below.TypeScript Configuration
For the best developer experience, especially when using subpath imports, ensure your tsconfig.json is configured correctly:
{
"compilerOptions": {
"moduleResolution": "bundler",
"resolvePackageJsonExports": true
}
}This enables proper resolution of subpaths like @indodev/toolkit/nik.
Quick Examples
NIK Validation and Parsing
import { validateNIK, parseNIK } from '@indodev/toolkit/nik';
// Validate format and checksum
validateNIK('3273051205900001');
// true
validateNIK('1234567890123456');
// false (invalid checksum)
// Parse to extract information
const info = parseNIK('3273051205900001');
// {
// province: { code: '32', name: 'Jawa Barat' },
// city: { code: '73', name: 'Kota Bandung' },
// gender: 'Male',
// birthDate: 1990-05-12T00:00:00.000Z,
// uniqueCode: 1
// }
info.province.name;
// 'Jawa Barat'
info.gender;
// 'Male'
info.birthDate.toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' });
// '12 Mei 1990'Phone Number Handling
import { validatePhone, formatPhoneNumber, parsePhone } from '@indodev/toolkit/phone';
// Validate
validatePhone('081234567890');
// true
validatePhone('099999999999');
// false (invalid operator prefix)
// Format in different styles
formatPhoneNumber('081234567890', 'domestic');
// '0812-3456-7890'
formatPhoneNumber('081234567890', 'international');
// '+62 812-3456-7890'
formatPhoneNumber('081234567890', 'e164');
// '+6281234567890'
// Parse to extract details
const phone = parsePhone('081234567890');
// {
// countryCode: '+62',
// operator: { name: 'Telkomsel', code: '812' },
// type: 'mobile',
// nationalNumber: '81234567890'
// }
phone.operator.name;
// 'Telkomsel'Currency Formatting
import { formatRupiah, formatCompact, toWords, splitAmount } from '@indodev/toolkit/currency';
// Standard formatting
formatRupiah(1500000);
// 'Rp 1.500.000'
formatRupiah(-1500000);
// '-Rp 1.500.000'
formatRupiah(1500000.50, { decimal: true });
// 'Rp 1.500.000,50'
// Compact format (dashboard-friendly)
formatCompact(1500000);
// 'Rp 1,5 juta'
formatCompact(1000000);
// 'Rp 1 juta' (not 'Rp 1,0 juta')
formatCompact(2500000000);
// 'Rp 2,5 miliar'
// Terbilang (number to words)
toWords(1500000);
// 'satu juta lima ratus ribu rupiah'
toWords(1500000.50, { withDecimals: true });
// 'satu juta lima ratus ribu rupiah koma lima puluh'
// Split amounts (installments, split bills)
splitAmount(1500000, 3);
// [500000, 500000, 500000]
splitAmount(10000, 3);
// [3334, 3333, 3333] (remainder distributed to first parts)
splitAmount(1000000, 2, { ratios: [70, 30] });
// [700000, 300000]Text Utilities
import { toTitleCase, slugify, maskText, toCamelCase } from '@indodev/toolkit/text';
// Title case with Indonesian grammar
toTitleCase('pt bank central asia tbk');
// 'PT Bank Central Asia Tbk'
toTitleCase('laskar pelangi: kisah nyata');
// 'Laskar Pelangi: Kisah Nyata'
// Slug generation with Indonesian conjunctions
slugify('Pria & Wanita');
// 'pria-dan-wanita'
slugify('Baju Murah Jakarta');
// 'baju-murah-jakarta'
// Mask sensitive data
maskText('08123456789', { pattern: 'middle', visibleStart: 4, visibleEnd: 3 });
// '0812****789'
maskText('user@example.com', { pattern: 'email' });
// 'us**@example.com'
// Case conversion
toCamelCase('hello-world');
// 'helloWorld'DateTime Utilities
import { formatDate, parseDate, toRelativeTime, getAge } from '@indodev/toolkit/datetime';
// Format dates with Indonesian locale
formatDate(new Date('2026-01-02'), 'long');
// '2 Januari 2026'
formatDate(new Date('2026-01-02'), 'full');
// 'Jumat, 2 Januari 2026'
// Parse Indonesian date format (DD-MM-YYYY)
parseDate('02-01-2026');
// Date(2026, 0, 2)
parseDate('02/01/2026');
// Date(2026, 0, 2)
// Relative time in Indonesian
toRelativeTime(new Date(Date.now() - 3600000));
// '1 jam yang lalu'
toRelativeTime(new Date(Date.now() + 86400000));
// 'Besok'
// Calculate age
getAge('1990-06-15', { asString: true });
// '35 Tahun 9 Bulan'
getAge(new Date('1990-06-15'));
// { years: 35, months: 9, days: 0 }Framework Integration
React / Next.js — Invoice Component
A realistic invoice component combining currency formatting, terbilang, and date display:
import { formatRupiah, toWords, calculateTax } from '@indodev/toolkit/currency';
import { formatDate } from '@indodev/toolkit/datetime';
interface InvoiceProps {
items: { name: string; qty: number; price: number }[];
invoiceNumber: string;
customerName: string;
}
export function Invoice({ items, invoiceNumber, customerName }: InvoiceProps) {
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
const tax = calculateTax(subtotal, 0.11); // 11% PPN
const total = subtotal + tax;
return (
<div className="invoice">
<h1>Invoice {invoiceNumber}</h1>
<p>Customer: {customerName}</p>
<table>
<thead>
<tr>
<th>Item</th>
<th>Qty</th>
<th>Price</th>
<th>Subtotal</th>
</tr>
</thead>
<tbody>
{items.map((item, i) => (
<tr key={i}>
<td>{item.name}</td>
<td>{item.qty}</td>
<td>{formatRupiah(item.price)}</td>
{/* formatRupiah(1500000) => 'Rp 1.500.000' */}
<td>{formatRupiah(item.price * item.qty)}</td>
</tr>
))}
</tbody>
</table>
<div className="totals">
<p>Subtotal: {formatRupiah(subtotal)}</p>
<p>PPN (11%): {formatRupiah(tax)}</p>
<p className="total">Total: {formatRupiah(total)}</p>
{/* formatRupiah(1110000) => 'Rp 1.110.000' */}
</div>
<p className="terbilang">
Terbilang: {toWords(total).toUpperCase()}
{/* toWords(1110000) => 'SATU JUTA SERATUS SEPULUH RIBU RUPIAH' */}
</p>
</div>
);
}Vue 3 — User Profile Card
A profile card with NIK parsing, phone formatting, and masked display:
<script setup>
import { ref, computed } from 'vue';
import { validateNIK, parseNIK } from '@indodev/toolkit/nik';
import { validatePhone, formatPhoneNumber } from '@indodev/toolkit/phone';
import { maskText } from '@indodev/toolkit/text';
const nik = ref('3273051205900001');
const phone = ref('081234567890');
const nikValid = computed(() => validateNIK(nik.value));
const nikInfo = computed(() => nikValid.value ? parseNIK(nik.value) : null);
// nikInfo.value?.province.name => 'Jawa Barat'
// nikInfo.value?.gender => 'Male'
const phoneValid = computed(() => validatePhone(phone.value));
const phoneFormatted = computed(() =>
phoneValid.value ? formatPhoneNumber(phone.value, 'international') : 'Invalid'
);
// phoneFormatted.value => '+62 812-3456-7890'
const phoneMasked = computed(() =>
phoneValid.value ? maskText(phone.value, { pattern: 'middle', visibleStart: 4, visibleEnd: 3 }) : ''
);
// phoneMasked.value => '0812***7890'
</script>
<template>
<div class="profile-card">
<div class="field">
<label>NIK</label>
<input v-model="nik" />
<span v-if="nikValid" class="valid">
{{ nikInfo?.province.name }} - {{ nikInfo?.gender }}
</span>
<span v-else class="error">NIK tidak valid</span>
</div>
<div class="field">
<label>Phone</label>
<input v-model="phone" />
<span v-if="phoneValid" class="valid">
{{ phoneFormatted }}
</span>
<span class="masked">
Display: {{ phoneMasked }}
</span>
</div>
</div>
</template>Coming From Other Libraries
If you are migrating from common utilities, here is the mapping:
| From | Use Instead | Example |
|---|---|---|
Lodash camelCase | toCamelCase from @indodev/toolkit/text | toCamelCase('hello-world') // ‘helloWorld’ |
Lodash snakeCase | toSnakeCase from @indodev/toolkit/text | toSnakeCase('helloWorld') // ‘hello_world’ |
numeral.js format | formatRupiah from @indodev/toolkit/currency | formatRupiah(1500000) // ‘Rp 1.500.000’ |
| libphonenumber-js | formatPhoneNumber from @indodev/toolkit/phone | formatPhoneNumber('081234567890', 'international') // ‘+62 812-3456-7890’ |
| slugify | slugify from @indodev/toolkit/text | slugify('Pria & Wanita') // ‘pria-dan-wanita’ |
Environment Support
Works in all modern environments:
- Browsers: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
- Node.js: 18.x or newer
- Runtimes: Deno, Bun, Cloudflare Workers
Next Steps
Explore the API
Dive into the specific documentation for each module:
- NIK Documentation
- NPWP Documentation
- Phone Documentation
- Email Documentation
- Vehicle Plate Documentation
- VIN Documentation
- Currency Documentation
- Text Documentation
- DateTime Documentation
Real-world Examples
Check out more complex patterns in our GitHub Examples .
Community
Found a bug or have a feature request? Let us know on GitHub Issues .