keesschepers/postcodenl-api-bundle
A Symfony bundle that integrates the Postcode.nl API for looking up Dutch addresses and postcodes. It provides service configuration and a simple client wrapper so you can validate postcodes and retrieve street, city and coordinates within your Symfony application.
Reality check before you install. This is a Symfony bundle, not a Laravel package. There is no extra.laravel manifest, no auto-discovery, and no service provider. If you are in a Laravel codebase, the correct "install" is not composer require keesschepers/postcodenl-api-bundle — it is treating the bundle as a ~50-line specification and writing your own adapter against the same REST endpoint. Copying request/response handling into a first-party class is cheaper than bridging Symfony DI and is under your maintenance.
If you are actually on Symfony (2/3-era):
composer require keesschepers/postcodenl-api-bundle
Register the bundle manually — it predates Flex, so add it to app/AppKernel.php (or config/bundles.php if it happens to be Flex-compatible).
Configure credentials. postcode.nl uses HTTP Basic auth where the API key is the username and the secret is the password. Keep them in parameters.yml/.env, never inline in bundle config.
Locate the real service before writing any code. Old bundles do not autowire:
bin/console debug:container | grep -i postcode
That gives you the actual service ID and the class to type-hint. Do not trust README examples from a 1-star repo — verify against the container.
First use case (95% of integrations): Dutch checkout / address-form prefill. User enters 1234AB + house number 42; you call the lookup and populate street and city into read-only fields.
Where to look first, in order:
Resources/config/services.xml — what is actually registered, what is the service ID.Wrap it; never call the client from a controller. The bundle client is a leaky abstraction tied to a specific Guzzle major and exception hierarchy. Define your own interface and inject the bundle service into an implementation:
interface AddressLookup {
/** @throws AddressNotFound */
public function lookup(string $postcode, string $houseNumber, ?string $addition = null): Address;
}
When the bundle dies — which it will — you swap one class, not every call site.
Normalize input before it reaches the client. postcode.nl expects exactly six alphanumeric characters in 1234 AB form. Users type 1234 ab, 1234-ab, 1234AB , lowercase, or with full-width characters. Normalize once, in your wrapper:
$postcode = strtoupper(preg_replace('/[^0-9A-Za-z]/', '', $postcode));
if (!preg_match('/^[1-9][0-9]{3}[A-Z]{2}$/', $postcode)) {
throw new InvalidPostcode();
}
Cheap local validation saves billable API calls and gives you a fast "did you mean" UX.
Laravel wiring. Bind the interface in a service provider and configure through config/services.php + .env:
// config/services.php
'postcode_nl' => [
'key' => env('POSTCODE_NL_KEY'),
'secret' => env('POSTCODE_NL_SECRET'),
'base_uri'=> 'https://api.postcode.nl/rest/addresses/',
'timeout' => 2,
],
// AppServiceProvider::register()
$this->app->bind(AddressLookup::class, PostcodenlAddressLookup::class);
Cache aggressively. Every lookup is a synchronous third-party HTTP call on a user-facing path, and the vendor bills per call. Key on normalized postcode + houseNumber + addition and set a TTL rather than caching forever — addresses change through demolition and renumbering:
return Cache::remember("pc:{$postcode}:{$number}:{$addition}", now()->addDays(30), fn () =>
$this->client->lookup($postcode, $number, $addition)
);
Distinguish "not found" from "error" in the mapping. Callers conflate these constantly and it produces terrible UX. AddressNotFound means the postcode is genuinely wrong or the property is new/unregistered — surface a validation hint. A transport failure or 5xx means you failed — keep the manual address fields editable and let the user proceed.
Test with fakes, not the network. In Laravel, Http::fake() on your adapter; in Symfony, swap the client binding for a stub in config/services_test.yml. Assert on the request shape (path, auth header, postcode format) and the mapping, because response field names are the part that silently drifts.
Sequence for a real rollout: confirm credentials/terms → spike the raw call (curl/Postman) to fix exact request/response shape → build the adapter behind the interface → add cache + timeout + fallback → ship behind a feature flag on one low-risk form → measure latency and failure rate → broaden.
bis, A, 1a) behave inconsistently. Not every valid postcode resolves to a residential address; design for "found partial" as a state.AddressLookup interface is what lets you answer that question later without a rewrite; the bundle is not.How can I help you explore Laravel packages today?