jira-datacenter-api-client
v1.2.0
Published
TypeScript client for Jira Data Center REST API
Maintainers
Readme
Jira Data Center API Client
A zero-dependency TypeScript client for the Jira Data Center REST API (and Jira Software Agile API).
Features
- Full TypeScript — every request and response is fully typed
- Read-mostly — covers major Jira Data Center read endpoints plus POST search/worklog helpers
- Chainable resources —
jira.issue('PROJ-42').comments()pattern - Metrics helpers — count/facet JQL buckets and lightweight user contribution activity
- Dual package — ships CJS + ESM, works in Node.js and browsers
- Zero runtime dependencies — uses native
fetchandURLSearchParams - Request events — hook into every HTTP request for logging and monitoring
- Semantic versioning — automated releases via Conventional Commits
Installation
npm install jira-datacenter-api-clientQuick Start
import { JiraClient } from 'jira-datacenter-api-client';
const jira = new JiraClient({
apiUrl: 'https://jira.example.com',
user: 'my-username',
token: 'my-personal-access-token',
});
// Search issues with JQL
const results = await jira.search({
jql: 'project = PROJ AND status = Open ORDER BY created DESC',
maxResults: 50,
});
console.log(results.total, 'issues found');
// Get a single issue
const issue = await jira.issue('PROJ-42');
console.log(issue.fields.summary);
// Get issue comments
const { comments } = await jira.issue('PROJ-42').comments();
// Get issue changelog
const changelog = await jira.issue('PROJ-42').changelog();
// Get a project with its components
const project = await jira.project('PROJ');
const components = await jira.project('PROJ').components();
// Get board sprints
const sprints = await jira.board(42).sprints({ state: 'active' });
// Get sprint issues
const sprintIssues = await jira.board(42).sprint(10).issues();
// Count issues without fetching pages
const bugCount = await jira.metrics.count({ jql: 'project = PROJ AND issuetype = Bug' });Authentication
The client uses HTTP Basic Authentication with a username and personal access token (PAT):
const jira = new JiraClient({
apiUrl: 'https://jira.example.com',
user: 'my-username',
token: 'my-personal-access-token',
// Optional — defaults shown:
apiPath: 'rest/api/latest',
agileApiPath: 'rest/agile/latest',
});To generate a personal access token, go to Jira → Profile → Personal Access Tokens.
API Reference
JiraClient
The main client. All methods return Promises.
Issues
// Search with JQL (GET)
await jira.search({ jql: 'project = PROJ', maxResults: 50, fields: 'summary,status' });
// Search with POST (supports larger field lists)
await jira.searchPost({ jql: 'project = PROJ', fields: ['summary', 'status', 'assignee'] });
// Get issue (await directly or call .get())
const issue = await jira.issue('PROJ-42');
const issue = await jira.issue('PROJ-42').get({ fields: 'summary,status,assignee' });
// Issue sub-resources
await jira.issue('PROJ-42').comments({ maxResults: 20 });
await jira.issue('PROJ-42').comment('10001');
await jira.issue('PROJ-42').worklogs();
await jira.issue('PROJ-42').worklog('20001');
await jira.issue('PROJ-42').changelog({ maxResults: 50 });
await jira.issue('PROJ-42').transitions();
await jira.issue('PROJ-42').remotelinks();
await jira.issue('PROJ-42').votes();
await jira.issue('PROJ-42').watchers();Projects
// List all projects
await jira.projects({ expand: 'description,lead' });
// Get a single project (await directly or call .get())
const project = await jira.project('PROJ');
// Project sub-resources
await jira.project('PROJ').components();
await jira.project('PROJ').versions();
await jira.project('PROJ').statuses(); // grouped by issue type
await jira.project('PROJ').roles(); // role name → URL map
await jira.project('PROJ').role(10002); // role with actorsBoards & Sprints (Jira Software)
// List boards
await jira.boards({ type: 'scrum', name: 'PROJ Board' });
// Get a board (await directly or chain)
const board = await jira.board(42);
// Board sub-resources
await jira.board(42).configuration();
await jira.board(42).sprints({ state: 'active' });
await jira.board(42).issues({ jql: 'status = "In Progress"' });
await jira.board(42).backlog({ maxResults: 50 });
// Sprint sub-resources
const sprint = await jira.board(42).sprint(10);
await jira.board(42).sprint(10).issues({ maxResults: 100 });Users
await jira.currentUser();
await jira.user('pilmee');
await jira.users({ username: 'john', maxResults: 10 });
// Find issues updated by one or more users, useful when Bitbucket slugs match Jira usernames
await jira.userActivity(['pilmee', 'john'], {
from: '-30d',
fields: ['summary', 'updated', 'status'],
maxResults: 50,
});Metrics & Activity
// Exact count via POST /search with maxResults: 0
await jira.metrics.count({ jql: 'project = PROJ AND statusCategory != Done' });
// Count issue buckets. Jira REST has no GROUP BY, so this runs one count query per value.
await jira.metrics.facet({
field: 'issuetype',
values: ['Bug', 'Story', 'Task'],
projects: ['PROJ'],
from: '2026-06-01',
jql: 'statusCategory != Done',
});
// Lightweight contribution counts for Jira usernames/user keys.
// Handy when Bitbucket slugs match Jira usernames.
await jira.metrics.userContributions(['pilmee', 'john'], {
projects: ['PROJ'],
from: '-30d',
include: ['created', 'updated', 'worklogIssues'],
});
// Incremental worklog feed for exact time-spent pipelines.
const changed = await jira.worklogsUpdated({ since: 1710000000000 });
const worklogs = await jira.worklogsList(changed.values.map((item) => item.worklogId));
await jira.worklogsDeleted({ since: 1710000000000 });Metadata
await jira.issuetypes();
await jira.issuetype('1');
await jira.priorities();
await jira.priority('3');
await jira.statuses();
await jira.status('In Progress');
await jira.fields();
await jira.issueLinkTypes();
await jira.favouriteFilters();
await jira.filter('10000');
await jira.component('10001');
await jira.version('20001');
await jira.versionIssueCounts('20001');
await jira.versionUnresolvedIssueCount('20001');Pagination
Most list endpoints return a paginated response:
interface PagedResponse<T> {
startAt: number;
maxResults: number;
total: number;
isLast?: boolean; // Agile endpoints
values: T[];
}To iterate through pages:
let startAt = 0;
const maxResults = 50;
let allIssues: JiraIssue[] = [];
while (true) {
const page = await jira.search({ jql: 'project = PROJ', startAt, maxResults });
allIssues = allIssues.concat(page.issues);
if (page.startAt + page.issues.length >= page.total) break;
startAt += maxResults;
}Request Events
Subscribe to every HTTP request for logging, metrics, or debugging:
jira.on('request', (event) => {
console.log(`[${event.method}] ${event.url} — ${event.durationMs}ms (${event.statusCode})`);
if (event.error) {
console.error('Request failed:', event.error.message);
}
});Event payload:
| Field | Type | Description |
|-------|------|-------------|
| url | string | Full URL requested |
| method | 'GET' \| 'POST' | HTTP method |
| startedAt | Date | Request start timestamp |
| finishedAt | Date | Request end timestamp |
| durationMs | number | Duration in milliseconds |
| statusCode | number? | HTTP status code |
| error | Error? | Error object if the request failed |
Error Handling
import { JiraApiError } from 'jira-datacenter-api-client';
try {
await jira.issue('PROJ-9999');
} catch (err) {
if (err instanceof JiraApiError) {
console.log(err.status); // 404
console.log(err.statusText); // 'Not Found'
console.log(err.message); // 'Jira API error: 404 Not Found'
}
}TypeScript Types
All domain types are exported:
import type {
JiraIssue,
JiraIssueFields,
JiraProject,
JiraUser,
JiraComment,
JiraWorklog,
JiraChangelogEntry,
JiraTransition,
JiraBoard,
JiraSprint,
JiraSearchResponse,
SearchParams,
PagedResponse,
// ... and many more
} from 'jira-datacenter-api-client';Contributing
See CONTRIBUTING.md for development guidelines.
