npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@postfinder/client

v0.1.1

Published

Find post offices, parcel lockers and post boxes near a point, and look up postcodes. Free and keyless.

Downloads

36

Readme

@postfinder/client

Post offices, parcel lockers and post boxes, as an API. Ask what is nearest a coordinate, look up what a postcode covers, or read a suburb's locations.

  • Free and keyless. No account, no quota to buy, nothing to configure
  • Runs anywhere there is fetch: a browser, a worker, a Node or Bun server, an SSR loader
  • Typed against the wire, with field names as the API sends them, so the docs and your editor agree
  • No dependencies
npm install @postfinder/client

Building a search box or a "what is near me" list? @postfinder/react and @postfinder/vue carry the debouncing, cancelling and keyboard handling for you. They stand alone, so use this one when you want the whole read surface: postcodes, suburbs, regions and category hubs.

Quick start

import { PostFinder } from "@postfinder/client";

const pf = new PostFinder();

const near = await pf.nearby({
  lat: -37.7404,
  lng: 144.9633,
  category: "post-offices",
  country: "australia",
});

for (const place of near) {
  console.log(place.name, place.address, `${place.distance_km * 1000} m`);
}

Nearest first, within 50km, at most 30 rows. That is the question "where do I post this", which is a different question from "list every post box in Victoria".

The six categories

import { CATEGORIES, type Category } from "@postfinder/client";

post-offices, post-boxes, express-post-boxes, parcel-lockers, drop-off-points, collection-points.

Category is a union, so a wrong name is a type error. It also throws before the request goes out, for the code paths a type cannot reach.

Typeahead

const controller = new AbortController();

const hits = await pf.search("coburg", { limit: 8, signal: controller.signal });
for (const hit of hits) {
  console.log(hit.kind, hit.name, hit.postcode);
}

Two characters minimum: below that it returns [] without asking, which is what the service answers anyway. Debounce by at least 150ms and pass a signal, so a superseded keystroke is cancelled rather than raced.

Linking back to the site

Every row carries the slugs its page path is built from, so no second request is needed:

import { hitPath, localityPath, placePath, metres } from "@postfinder/client";

hitPath(hits[0]);                        // /en/australia/victoria/coburg/
placePath("australia", "victoria", "coburg", place.brand, place.slug);
metres(place.distance_km);               // 420

A location's path carries its brand as a segment, and a place with no brand is filed under unbranded. A search row does not carry the brand key, so a place hit links to its suburb; place() returns the exact path.

Postcodes

A postcode is not a suburb. 3058 is Coburg, Coburg North and Merlynston, and an address in any of them is written with the same four digits.

const detail = await pf.postcode("australia", "3058");
detail.localities.map((l) => l.name);   // ["Coburg", "Coburg North", "Merlynston"]

The whole country comes in one response, and it is meant to be kept:

import { suburbsIn } from "@postfinder/client";

const index = await pf.postcodes("australia");   // one request
suburbsIn(index, "3058");                        // no request, and no rescan

suburbsIn builds its map on first use and keeps it beside the index in a WeakMap, so resolving a column of ten thousand postcodes is one pass over the country rather than ten thousand walks through every state. The response itself is handed back untouched.

A location, a suburb, a country

const place = await pf.place("k7m2p9x4");
const suburb = await pf.locality("australia", "victoria", "coburg");

await pf.countries();
await pf.country("australia");
await pf.region("australia", "victoria", { limit: 100, offset: 0 });
await pf.category("australia", "parcel-lockers", { region: "victoria" });

A place's public id is permanent. It is minted once and never derived from a source record, so a feed that renumbers its rows does not change it. Store the id, not the name or the path.

Errors

import { NotFound, RateLimited, PostFinderError } from "@postfinder/client";

try {
  await pf.place(storedId);
} catch (err) {
  if (err instanceof NotFound) return null;   // retired, or never there
  throw err;
}

NotFound is ordinary rather than a failure: a suburb with no locations has no page, and a location that closed is retired. Every error carries the status, title and detail the service sent, and RateLimited carries retryAfter when the service said.

In an SSR loader

// app/routes/nearest.tsx
import { PostFinder } from "@postfinder/client";

const pf = new PostFinder();

export async function loader({ request }: { request: Request }) {
  const url = new URL(request.url);
  return {
    near: await pf.nearby({
      lat: Number(url.searchParams.get("lat")),
      lng: Number(url.searchParams.get("lng")),
      category: "parcel-lockers",
      country: "australia",
      signal: request.signal,
    }),
  };
}

Passing request.signal means a visitor who navigates away takes their request with them.

Being a good citizen

The API is free and asks for care in return: around a thousand requests a month from one address, answers kept rather than re-fetched, typing debounced. If you need more, say what you are building at postfinder.io/en/contact/.

const pf = new PostFinder({ contact: "https://example.com/about-our-bot" });

Attribution

Locations come from OpenStreetMap (ODbL), localities and postcodes from GeoNames (CC BY 4.0). If you publish what you get back, you carry those credits with it. The sources page names each one.

Also available

| | | |---|---| | React | @postfinder/react | | Vue | @postfinder/vue | | Python | postfinder | | Go | postfinder-go |

Licence

MIT.