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

@nicholasf/whurl

v0.1.0

Published

GraphQL test double and contract layer

Readme

whurl

whurl intercepts HTTP calls in tests at the network layer — no vi.mock() calls, your real client code runs. For GraphQL endpoints it validates queries and data shapes against your schema using graphql-js, the GraphQL Foundation's reference implementation. For any other endpoint — REST APIs, OAuth providers, external services — it works as a thin wrapper over MSW. Either way, every intercepted call can be recorded as a Hurl file and replayed against a real backend later.

If you've wanted a GraphQL schema to act as a contract between frontend and backend — the way a Swagger file does for REST — whurl is built for that, although it solves the problem differently.

whurl combines two libraries: msw (Mock Service Worker) for HTTP interception in tests, and Hurl for contract verification against real backends. The name comes from Worker and Hurl.

import { render, screen } from '@testing-library/react'
import { describe, it, expect, beforeEach } from 'vitest'
import { registerWithSchema, specify, reset } from 'whurl'
import { DashboardPage } from './pages/DashboardPage'
import { schema } from '../tests/schema'

beforeEach(() => {
  reset()
  registerWithSchema('http://localhost:3000/graphql', schema)
})

describe('DashboardPage', () => {
  it('renders the authenticated user', async () => {
    specify('Me', {  // intercepts the 'Me' query DashboardPage fires on mount
      me: { id: '1', name: 'Darth Vader', email: '[email protected]' }
    })

    render(<DashboardPage />)

    expect(await screen.findByText('Darth Vader')).toBeInTheDocument()
  })
})

Then, later on, after you are happy with your mock logic, you can whurl it at the backend!

Run your tests with WHURL=true and whurl writes a Hurl file for every consumed specification:

WHURL=true vitest run

The Me specification above produces whurl/2026-06-25T11-38-15-Me.hurl:

POST http://localhost:3000/graphql
Content-Type: application/json

{
  "query": "query Me { me { id name email } }"
}

The backend team runs this against their real implementation — no frontend required:

hurl whurl/*.hurl

Setup

Call start() and stop() in your test setup file:

// tests/setup.ts
import { beforeAll, afterAll } from 'vitest'
import { start, stop } from 'whurl'

beforeAll(() => start())
afterAll(() => stop())
// vitest.config.ts
export default {
  test: {
    setupFiles: ['./tests/setup.ts'],
  },
}

Registering endpoints

For a plain HTTP endpoint:

import { register } from 'whurl'

register('http://localhost:3000/api/accounts')

For a GraphQL endpoint, provide the schema string. whurl parses it immediately and throws if it is invalid:

import { registerWithSchema } from 'whurl'

registerWithSchema('http://localhost:3000/graphql', `
  type User { id: ID! name: String! email: String! }
  type Query { me: User }
`)

Specifications

A specification declares what a GraphQL operation should return. whurl validates the response shape against the registered schema and intercepts the matching request, returning the specified data.

import { registerWithSchema, specify } from 'whurl'

registerWithSchema('http://localhost:3000/graphql', schema)

specify('Me', {
  me: { id: '1', name: 'Darth Vader', email: '[email protected]' }
})

Operations must be named. whurl matches specifications to intercepted requests by operation name:

# ✓ whurl can match this
query Me {
  me { id name email }
}

# ✗ whurl will throw at intercept time
{
  me { id name email }
}

By default a specification is matched once and then exhausted. Chain .repeat(n) to allow it to be matched more times:

specify('Me', { me: { id: '1', name: 'Darth Vader', email: '[email protected]' } })          // matched once
specify('Me', { me: { id: '1', name: 'Darth Vader', email: '[email protected]' } }).repeat(3) // matched three times

Call reset() between tests to clear all specifications and registered endpoints:

import { beforeEach } from 'vitest'
import { reset } from 'whurl'

beforeEach(() => reset())

Plain HTTP endpoints

whurl can intercept any HTTP endpoint, not just GraphQL. This is useful for mocking OAuth providers, REST APIs, or any other HTTP dependency your components talk to during tests.

Register a plain endpoint with register(), then declare specifications using the three-argument form — operation name, HTTP method, and response data:

import { register, specify } from 'whurl'

register('http://auth.example.com/oauth/token')

specify('ExchangeToken', 'POST', {
  access_token: 'sith-token-abc123',
  token_type: 'Bearer',
  expires_in: 3600,
})

whurl is just MSW here — register + specify is a thin DSL over an MSW handler. The intercept, the response envelope, the lifecycle — all MSW. whurl adds the operation name as a label and the .repeat(n) lifetime on top.

Hurl export

When tests run with WHURL=true, whurl writes a Hurl file for each consumed specification:

WHURL=true vitest run

This produces files in whurl/:

whurl/2026-06-25T11-38-15-Me.hurl
whurl/2026-06-25T11-38-15-CreatePost.hurl

The backend team can then run these against their implementation — no frontend required:

hurl whurl/*.hurl

Query variables are expressed as Hurl variables so the backend team can supply real values via a variables file:

hurl whurl/*.hurl --variables-file whurl/vars.env

Interceptor

HTTP interception is behind an Interceptor interface. The current implementation uses MSW, but the interface allows a different worker implementation to be swapped in without changing any test code:

interface Interceptor {
  start(): void
  stop(): void
  reset(): void
}

This means whurl is not tied to MSW specifically — any library that can intercept HTTP requests and return a response can be wired in by implementing this interface.

Verbose mode

Set WHURL_VERBOSE=true to log each matched specification as a Hurl request to stdout as tests run:

WHURL_VERBOSE=true vitest run

Output for a GraphQL spec:

POST http://localhost:3000/graphql
Content-Type: application/json

{
  "query": "query Me { me { id name email } }"
}

Output for a plain HTTP spec:

GET http://localhost:3000/api/accounts

Verbose mode and WHURL=true file export are independent — both can be active at once.

Status

Early development. The API is not yet stable and will change before the first release.