de-plz
v0.1.0
Published
TypeScript library for German postal codes (PLZ) — validation and federal state lookup.
Maintainers
Readme
de-plz
German postal codes (Postleitzahlen / PLZ) — validation, federal-state lookup, and routing-zone helpers, fully typed.
Why this?
Almost every German form, checkout, or address feature needs to validate a PLZ and often to map it to a federal state (Bundesland). Existing packages tend to bundle a huge full-address dataset, go unmaintained, or ship without real TypeScript types. de-plz does one thing well: it takes a PLZ and tells you whether it's valid, which routing zone it belongs to, and its most likely federal state — small, fully typed, and with zero runtime dependencies.
Installation
npm install de-plzShips both ESM and CommonJS builds with bundled type declarations. Node 18+.
Accuracy & Known Limitations
German postal codes don't align perfectly with federal-state borders. PLZ ranges follow historical Deutsche Post routing regions (Leitregionen), not administrative boundaries, so a range that is overwhelmingly one state can still contain a handful of PLZs that belong to a neighbour.
This library maps each PLZ range to its statistically dominant state. For the large majority of postal codes that is exactly right — but near a region edge, a result may be off by one state. Every range is therefore reported with confidence: 'majority'; use lookupPLZ() instead of getStateFromPLZ() when you need to read that field.
Concrete known exceptions in the current data:
- Hamburg (
20000–22999) resolves toDE-HH, but large parts of the21xxx/22xxxband are actually Niedersachsen (e.g. Stade, Buchholz) or Schleswig-Holstein (e.g. Norderstedt, Ahrensburg). - Saarland (
66000–66999) resolves toDE-SL, but much of the region is the Rheinland-Pfalz Westpfalz — e.g.66482Zweibrücken and66953Pirmasens areDE-RP. - Ulm (
89000–89999) resolves toDE-BW, but a substantial part is Bavarian Schwaben — e.g.89231Neu-Ulm and89407Dillingen areDE-BY. - Individual towns that sit across a region edge, including
04600Altenburg (DE-TH),17268Templin and19322Wittenberge (DE-BB),27568Bremerhaven (DE-HB),36433Bad Salzungen and37308Heilbad Heiligenstadt (DE-TH),63739Aschaffenburg (DE-BY),68519Viernheim (DE-HE),88131Lindau (DE-BY),96515Sonneberg (DE-TH), and97877Wertheim (DE-BW).
Format validity is exact: a PLZ is valid only as five digits in the assigned range 01067–99998 (the 00xxx band was never assigned by Deutsche Post). State lookup is best-effort as described above.
Quick start
import { isValidPLZ, getStateFromPLZ, formatPLZ, getPLZZone } from 'de-plz'
// Validate format (5 digits, within the assigned range)
isValidPLZ('10115') // true
isValidPLZ('1011') // false — too short
isValidPLZ('00000') // false — 00xxx was never assigned
// Most likely federal state for a PLZ (ISO 3166-2 code)
getStateFromPLZ('10115') // 'DE-BE' — Berlin
getStateFromPLZ('80331') // 'DE-BY' — Bayern
getStateFromPLZ('99998') // 'DE-TH' — Thüringen
// Normalize a PLZ string (trims whitespace; throws on invalid input)
formatPLZ(' 10115 ') // '10115'
formatPLZ('1011') // throws — invalid input, not silently padded
// Leading-digit Deutsche Post routing zone (0–9)
getPLZZone('10115') // 1
getPLZZone('80331') // 8API
isValidPLZ(input): boolean
Whether the string is a structurally valid German PLZ: exactly five digits within the assigned range 01067–99998. This checks format only — it does not guarantee the specific PLZ has been assigned to a delivery area.
isValidPLZ('80331') // true
isValidPLZ('1011A') // falsegetStateFromPLZ(input): GermanState | null
The most likely federal state for the PLZ, as an ISO 3166-2 code, or null if the input isn't valid or falls in an unassigned gap. See Accuracy & Known Limitations — this is a best-effort lookup.
getStateFromPLZ('50667') // 'DE-NW' — Köln
getStateFromPLZ('abc') // nulllookupPLZ(input): PLZLookupResult
Full result with metadata — validity, state, routing zone, and confidence in one object. Use this when you need the confidence field, not just the state.
lookupPLZ('10115')
// → { plz: '10115', valid: true, state: 'DE-BE', zone: 1, confidence: 'majority' }
lookupPLZ('abc')
// → { plz: 'abc', valid: false, state: null, zone: null, confidence: null }formatPLZ(input): string
Trims surrounding whitespace and returns the normalized PLZ. Throws if the result isn't a valid PLZ — it never pads or truncates, since a malformed code should be caught rather than guessed at.
formatPLZ(' 10115 ') // '10115'
formatPLZ('1011') // throws ErrorgetPLZZone(input): number | null
The Deutsche Post routing zone — the leading digit of the PLZ (0–9) — or null for invalid input.
getPLZZone('01067') // 0
getPLZZone('80331') // 8Types
type GermanState =
| 'DE-BW' | 'DE-BY' | 'DE-BE' | 'DE-BB' | 'DE-HB' | 'DE-HH'
| 'DE-HE' | 'DE-MV' | 'DE-NI' | 'DE-NW' | 'DE-RP' | 'DE-SL'
| 'DE-SN' | 'DE-ST' | 'DE-SH' | 'DE-TH'
type PLZZone = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
interface PLZRange {
from: number
to: number
state: GermanState
confidence: 'exact' | 'majority'
}
interface PLZLookupResult {
plz: string
valid: boolean
state: GermanState | null
zone: PLZZone | null
confidence: 'exact' | 'majority' | null
}Federal states
State lookups return ISO 3166-2 codes:
| Code | State | Code | State |
|---|---|---|---|
| DE-BW | Baden-Württemberg | DE-NI | Niedersachsen |
| DE-BY | Bayern | DE-NW | Nordrhein-Westfalen |
| DE-BE | Berlin | DE-RP | Rheinland-Pfalz |
| DE-BB | Brandenburg | DE-SL | Saarland |
| DE-HB | Bremen | DE-SN | Sachsen |
| DE-HH | Hamburg | DE-ST | Sachsen-Anhalt |
| DE-HE | Hessen | DE-SH | Schleswig-Holstein |
| DE-MV | Mecklenburg-Vorpommern | DE-TH | Thüringen |
Notes on the data
- Source. Ranges follow Deutsche Post Leitregionen (the first two digits of a PLZ define a routing region), cross-referenced with state assignments from the Bundesamt für Kartographie und Geodäsie (BKG).
- Assigned range. The lowest assigned PLZ is
01067(Dresden) and the highest is99998; the00xxxband is unassigned. A few Leitregions (e.g.05xxx) are unassigned gaps and returnnull. - Numbers, not strings. Lookups compare numeric values internally, so a leading zero is significant only for format validation —
01067is the numeric value1067.
Contributing
Issues and pull requests are welcome — especially corrections to the range data backed by an official source. To work on the library:
npm install
npm test # watch mode
npm run test:run # single run
npm run typecheck
npm run lint
npm run buildLicense
MIT © pouya1364
Companion package to dach-holidays — public holidays for the DACH region (Germany, Austria, Switzerland), every state, Bundesland, and canton.
