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

@rvaiglobal/github-issue-creator

v0.1.1

Published

Tiny zero-dependency backend library for GitHub issue operations (create, get, list, update) with a PAT read from the environment.

Readme

github-issue-creator

A tiny, zero-dependency backend library for GitHub issue operations. It reads a Personal Access Token and a default repository from the environment and exposes five functions — create, get, list, list-all, and update issues.

  • Zero runtime dependencies — uses the native fetch in Node >=20.19.
  • Backend only — no CLI, no server, no browser, no UI. Your app owns the route and auth.
  • Token stays server-side — never returned in a result, never logged.
  • Raw GitHub objects — returns exactly what the GitHub REST API returns; no custom schema to learn.

Table of contents

Install

npm install github-issue-creator
# or
pnpm add github-issue-creator
# or
yarn add github-issue-creator

Requires Node >= 20.19 (uses the built-in global fetch). ESM only — import, not require.

Quick start

import { createIssue } from "github-issue-creator";

// With GITHUB_TOKEN and GITHUB_REPOSITORY set in the environment:
const issue = await createIssue({ title: "Login broken", body: "Steps to reproduce..." });
console.log(issue.number, issue.html_url);

Configuration

The token and the target repository are read from two environment variables:

| Variable | Example | Meaning | |---|---|---| | GITHUB_TOKEN | ghp_xxx / github_pat_xxx | Personal Access Token used for every request. | | GITHUB_REPOSITORY | octocat/hello-world | Default repo in owner/repo form (the GitHub Actions convention). |

Set them on your backend (e.g. a .env file loaded with node --env-file=.env, your process manager, or your host's secret store):

GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxx
GITHUB_REPOSITORY=octocat/hello-world

Per-call overrides. Every function accepts optional connection fields, so the env vars are not mandatory — you can pass them explicitly instead:

await createIssue({ title: "x", token: process.env.OTHER_TOKEN, repo: "acme/other" });
await listIssues({ owner: "acme", repo: "web" });

If a token cannot be resolved (no env var and no override) the function throws a configuration error before making any network call. The same applies to a missing or malformed repository.

Creating a token (PAT)

The minimum permission for all five functions is Issues: Read and write.

Fine-grained token (recommended)

  1. https://github.com/settings/personal-access-tokens/new
  2. Repository accessOnly select repositories → choose your repo(s) (or All repositories).
  3. Permissions → Repository → Issues → Read and write (Metadata: Read-only is added automatically).
  4. Generate token and copy it.

Classic token

  1. https://github.com/settings/tokensTokens (classic)Generate new token.
  2. Select the repo scope (or public_repo for public repos only).
  3. Generate and copy it.

Treat the token as a secret. Keep it server-side, never commit it, never expose it to a browser. Add .env to your .gitignore.

API reference

All functions are async, repo-scoped, and return GitHub's raw response objects (so issue.number, issue.html_url, issue.state, issue.labels, etc. are GitHub's own fields). Any non-2xx response throws a GitHubApiError.

Every function takes a single options object. Beyond the fields listed per function, all of them also accept the shared connection options (token, owner, repo, baseUrl).

createIssue(options)

Creates an issue. Optional fields are omitted from the request when not provided.

| Field | Type | Required | Description | |---|---|---|---| | title | string | ✅ | Issue title. | | body | string | — | Markdown body. | | labels | string[] | — | Label names. | | assignees | string[] | — | GitHub usernames to assign. | | milestone | number | — | Milestone number (not title). |

Returns: the created issue object. Throws on validation errors (e.g. a nonexistent assignee → 422) or missing permission (403).

const issue = await createIssue({
  title: "Checkout fails on Safari",
  body: "Tested on Safari 17. Stack trace attached.",
  labels: ["bug", "checkout"],
  assignees: ["octocat"],
  milestone: 3,
});
console.log(issue.number);   // e.g. 128
console.log(issue.html_url); // https://github.com/owner/repo/issues/128

getIssue(options)

Fetches a single issue by number.

| Field | Type | Required | Description | |---|---|---|---| | number | number | ✅ | Issue number (positive integer). |

Returns: the issue object. Throws 404 if the issue does not exist.

const issue = await getIssue({ number: 42 });
console.log(issue.state); // "open" | "closed"

listIssues(options)

Returns one page of issues for the repository.

| Field | Type | Default | Description | |---|---|---|---| | page | number | 1 | Page number. | | perPage | number | 30 | Items per page (max 100). | | state | string | open | open | closed | all. | | labels | string | — | Comma-separated label names, e.g. "bug,ui". | | assignee | string | — | Username, none, or *. | | creator | string | — | Username of the issue author. | | mentioned | string | — | Username mentioned in the issue. | | milestone | string \| number | — | Milestone number, none, or *. | | since | string | — | ISO 8601 timestamp; only issues updated at/after. | | sort | string | created | created | updated | comments. | | direction | string | desc | asc | desc. |

Returns: { issues, hasNextPage, nextPage }

| Property | Type | Description | |---|---|---| | issues | object[] | The page of issue objects. | | hasNextPage | boolean | Whether another page exists. | | nextPage | number \| null | The next page number, or null. |

let page = 1;
do {
  const { issues, hasNextPage, nextPage } = await listIssues({ state: "all", page, perPage: 100 });
  for (const issue of issues) console.log(issue.number, issue.title);
  page = nextPage;
  if (!hasNextPage) break;
} while (page);

Pull requests are included. GitHub's list-issues endpoint returns PRs as issues, and this library does not filter them out. Skip them with issues.filter((i) => !i.pull_request) if you only want true issues.

listAllIssues(options)

Auto-paginates and returns every matching issue as a single array.

Accepts the same filters as listIssues, plus:

| Field | Type | Default | Description | |---|---|---|---| | maxItems | number | Infinity | Stop after collecting this many items. | | perPage | number | 100 | Page size used while paginating. |

Returns: object[] — all matching issues (capped at maxItems).

const open = await listAllIssues({ state: "open" });
console.log(`${open.length} open issues`);

// Cap the work on large repos:
const recent = await listAllIssues({ state: "all", sort: "updated", maxItems: 200 });

updateIssue(options)

Updates an existing issue. Only the fields you pass are sent; everything else is left unchanged.

| Field | Type | Required | Description | |---|---|---|---| | number | number | ✅ | Issue number. | | title | string | — | New title. | | body | string | — | New body. | | state | string | — | open or closed (close/reopen). | | labels | string[] | — | Replaces the label set. | | assignees | string[] | — | Replaces the assignee set. | | milestone | number \| null | — | Milestone number, or null to clear. |

Returns: the updated issue object.

// Close an issue
await updateIssue({ number: 42, state: "closed" });

// Reopen and relabel
await updateIssue({ number: 42, state: "open", labels: ["bug", "regression"] });

Connection options

Every function accepts these optional fields to override the environment:

| Field | Type | Default | Description | |---|---|---|---| | token | string | process.env.GITHUB_TOKEN | PAT for this call. | | repo | string | from GITHUB_REPOSITORY | "owner/repo". | | owner | string | — | Owner; use together with a bare repo name. | | baseUrl | string | https://api.github.com | API base URL — set for GitHub Enterprise. |

Repository resolution order: explicit { owner, repo } → explicit { repo: "owner/repo" }GITHUB_REPOSITORY.

GitHubApiError

Thrown on any non-2xx response.

| Property | Type | Description | |---|---|---| | message | string | GitHub's error message, verbatim. | | status | number | HTTP status (e.g. 403, 404, 422). | | errors | object[] | GitHub's field-level errors, when present. | | documentationUrl | string | GitHub's docs link, when present. |

Error handling

import { createIssue, GitHubApiError } from "github-issue-creator";

try {
  await createIssue({ title: "New bug" });
} catch (err) {
  if (err instanceof GitHubApiError) {
    // GitHub's exact message and status — surface it to your user or logs
    console.error(`GitHub ${err.status}: ${err.message}`);
    if (err.errors) console.error(err.errors);
  } else {
    // Configuration error (missing token/repo) or a network failure
    console.error(err.message);
  }
}

Common statuses: 401 bad credentials · 403 token lacks permission · 404 repo/issue not visible to the token · 410 issues disabled on the repo · 422 validation failed (see errors).

Using it from a frontend

This package exposes functions, not HTTP routes — that is deliberate, it keeps the token on your server. If a browser needs to trigger an operation, call the library from your own backend route and let your app handle auth:

Express

import { createIssue, GitHubApiError } from "github-issue-creator";

app.post("/api/issues", async (req, res) => {
  try {
    // ...your authentication + input validation here...
    const issue = await createIssue({ title: req.body.title, body: req.body.body });
    res.status(201).json({ number: issue.number, url: issue.html_url });
  } catch (err) {
    const status = err instanceof GitHubApiError ? err.status : 500;
    res.status(status).json({ error: err.message });
  }
});

Next.js (app router)app/api/issues/route.js

import { createIssue } from "github-issue-creator";

export async function POST(req) {
  const { title, body } = await req.json();
  const issue = await createIssue({ title, body });
  return Response.json({ number: issue.number, url: issue.html_url }, { status: 201 });
}

Notes & caveats

  • Raw objects. Returns are GitHub's response objects unchanged — refer to the GitHub Issues REST docs for the full field list.
  • PRs in lists. listIssues/listAllIssues include pull requests (GitHub treats them as issues). Filter on i.pull_request if you don't want them.
  • List vs. read consistency. A just-created issue can take a moment to appear in list results (GitHub's list index is eventually consistent), while getIssue reflects it immediately.
  • No retries/throttling. Calls are made once. If you need rate-limit backoff or retries, wrap the functions in your app.
  • No deletes. GitHub has no issue-delete API; close via updateIssue instead.

License

MIT