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

@sdxc/problem

v2026.10.6

Published

Build, detect and parse RFC 9457 problem details, cataloged per API

Downloads

1,043

Readme

@sdxc/problem

Build, detect and parse RFC 9457 problem details (application/problem+json), and declare an API's problem types in one catalog.

Installation

npm add @sdxc/problem

Extension members are validated with remix/data-schema or any other Standard Schema, and results come back as @sdxc/result values; both install alongside this package.

Usage

Answer With A Problem

import { problem } from "@sdxc/problem";

return problem({ status: 404 });
// 404, Content-Type: application/problem+json
// {"type":"about:blank","title":"Not Found","status":404}

A status alone is a complete document: type defaults to about:blank and title to the status phrase. Add type, title, detail, instance and extensions as needed:

return problem(
	{
		status: 403,
		type: "https://example.com/probs/out-of-credit",
		title: "You do not have enough credit.",
		detail: "Your current balance is 30, but that costs 50.",
		extensions: { balance: 30 },
	},
	{ headers: { "Cache-Control": "no-store" } },
);

Extensions are written at the document's top level, and one named like a standard member never replaces it.

Declare A Catalog

import { defineProblems } from "@sdxc/problem";
import * as s from "remix/data-schema";

export const problems = defineProblems("https://docs.example.com/errors/", {
	notFound: { slug: "not-found", status: 404, title: "The resource does not exist" },
	outOfCredit: {
		slug: "out-of-credit",
		status: 403,
		title: "You do not have enough credit",
		extensions: s.object({ balance: s.number() }),
	},
});

return problems.notFound({ detail: "No article has that slug." });
return problems.outOfCredit({ extensions: { balance: 30 } });

Each entry becomes a builder that writes its type (the base URL followed by the slug), status and title, so a call site supplies only detail, instance and the extensions its schema types. The base URL must end in /.

Read A Problem

import { isProblem, parseProblem } from "@sdxc/problem";
import { isSuccess } from "@sdxc/result";

if (isProblem(response)) {
	let result = await parseProblem(response);
	if (isSuccess(result)) result.data.type; // "https://example.com/probs/out-of-credit"
}

With a catalog, parse names the entry a response belongs to and validates its extensions:

let result = await problems.parse(response);

if (isSuccess(result) && problems.is(result.data, "outOfCredit")) {
	result.data.extensions.balance; // number
}

A type outside the catalog still parses, with name: null, as RFC 9457 requires clients to accept problem types they do not know.

Report Validation Failures

import { issuesFrom, validationProblem } from "@sdxc/problem";
import * as s from "remix/data-schema";

let result = s.parseSafe(schema, body);
if (!result.success) return validationProblem(issuesFrom(result.issues));
// 422 with {"errors":[{"pointer":"/user/email","code":"invalid","message":"..."}], ...}

API

problem(options, init?)

Returns a Response whose status line and body status both come from options.status, with Content-Type: application/problem+json. init adds headers.

stringify(options)

The document's JSON text, for writing a problem somewhere other than a Response.

defineProblems(base, entries)

A catalog: one builder per entry, plus parse(response), is(problem, name) and entries(), which lists every entry with its resolved type for rendering an error reference, plus the extensions schema of an entry declared with one, for documenting its members. Entries cannot be named parse, is or entries.

isProblem(message)

Whether a Request or Response declares application/problem+json, ignoring parameters and case. The body is left unread.

parseProblem(response, options?)

Reads a problem Response into a Result<Problem, ProblemParseError>. Absent members take their RFC defaults, and the status line wins over the body's status. Pass options.extensions, a synchronous Standard Schema, to validate and type the extensions.

parse(text, options?)

The same, from JSON text; options.status stands in for the status line.

validationProblem(issues, options?)

A 422 problem whose errors extension lists each invalid field. options sets any standard member, including a different status.

issuesFrom(source, code?)

Converts Standard Schema issues, or an error carrying them in issues, into errors entries, turning each path into a JSON Pointer. Every entry gets code, "invalid" by default.

toPointer(path)

Formats an issue path as an RFC 6901 JSON Pointer, escaping ~ and /.

ISSUES_SCHEMA

The schema for the errors extension, for s.object({ errors: ISSUES_SCHEMA }). It also implements Standard JSON Schema, so a tool that documents a catalog can describe the extension.

ProblemParseError

Why a document is not a problem. issues holds the extension schema's issues when that is the reason.

PROBLEM_MEDIA_TYPE, ABOUT_BLANK

"application/problem+json" and "about:blank".

Types

Problem<Extensions> is a parsed document, with detail and instance as string | null and extension members under extensions. ProblemOptions<Extensions> is what a writer passes, and ProblemIssue is one errors entry.

Pattern: Share A Catalog Between A Server And Its Client

// api-problems.ts, imported by both
import { ISSUES_SCHEMA, defineProblems } from "@sdxc/problem";
import * as s from "remix/data-schema";

export const apiProblems = defineProblems("https://docs.example.com/errors/", {
	notFound: { slug: "not-found", status: 404, title: "The resource does not exist" },
	validationFailed: {
		slug: "validation-failed",
		status: 422,
		title: "The request body is invalid",
		extensions: s.object({ errors: ISSUES_SCHEMA }),
	},
});
// server
import { issuesFrom } from "@sdxc/problem";
import * as s from "remix/data-schema";

let result = s.parseSafe(schema, body);
if (!result.success) {
	return apiProblems.validationFailed({ extensions: { errors: issuesFrom(result.issues) } });
}
// client
import { isSuccess } from "@sdxc/result";

let response = await fetch(url, { method: "POST", body });
if (!response.ok) {
	let result = await apiProblems.parse(response);
	if (isSuccess(result) && apiProblems.is(result.data, "validationFailed")) {
		for (let issue of result.data.extensions.errors) showError(issue.pointer, issue.message);
	}
}

Versioning

Releases are dated rather than semantic. A version is the UTC date it was published, written YYYY.M.D, so 2026.9.4 is the release from 4 September 2026. At most one release goes out per day.

Those numbers say when, not what: a later date means a later release and carries no compatibility promise. Any release may change or remove an export.

Depend on one exact date, and move it when you are ready to take the change:

{
	"dependencies": {
		"@sdxc/problem": "2026.9.4"
	}
}

A caret or tilde range reads the date as major, minor and patch, so it accepts every later release in the same year. An exact version keeps the upgrade yours to schedule.

License

MIT

Author

Sergio Xalambrí