A TypeSpec specification of the Redmine REST API, versioned by Redmine release, with a generated OpenAPI description and TypeScript client. Integration tests check the spec against real Redmine servers, from 3.1 to 7.0.
Documentation: crawlercode.github.io/redmine-api-client: guides, the client and React reference, and the API reference of every Redmine version.
| Folder | Content |
|---|---|
spec/ |
The TypeSpec spec. One folder per API resource (models.tsp, dtos.tsp, queries.tsp, service.tsp), shared models in shared/, Redmine releases in server-versions.tsp. |
schema/openapi/ |
Generated OpenAPI 3.1 documents, one per Redmine version (openapi.<version>.yaml). |
generated/redmine-api-client/ |
Generated TypeScript client (redmine-api-client-generated), never edited by hand and not published. |
packages/redmine-api-client/ |
The published TypeScript client (redmine-api-client): bundles the generated client and adds hand-written code. |
packages/redmine-api-client-react/ |
TanStack Query options for React (redmine-api-client-react), one module per resource. |
docs/ |
The documentation site (Fumadocs, deployed to GitHub Pages). pnpm docs:dev starts it locally. |
tests/ |
Integration tests against Redmine in Docker. |
dev/ |
Scratch script for trying the client against your own Redmine. |
Requires Node.js 24+, pnpm and, for the tests, Docker.
pnpm install
pnpm build # compile the spec, then build the clientDuring spec work, pnpm spec:compile:watch recompiles on every change.
The spec is versioned with @typespec/versioning. RedmineServerVersion in spec/server-versions.tsp lists the Redmine releases that changed the API, and operations, properties and query parameters are marked with @Versioning.added(...) where they first appeared. The OpenAPI emitter writes one document per version, each showing the API of that release.
The version a feature was added in is taken from the Redmine source (app/views/**/*.api.rsb, controllers and routes of each x.y-stable branch) and verified by the tests. Unreleased features (Redmine master) are left out.
pnpm add redmine-api-client
pnpm add redmine-api-client-react @tanstack/react-query # TanStack Query options for Reactimport { IssueQueryInclude, RedmineClient } from "redmine-api-client";
const client = new RedmineClient("https://redmine.example.com", {
key: process.env.REDMINE_API_KEY!,
});
const { issue } = await client.issuesClient.getIssue(42, {
include: [IssueQueryInclude.Journals],
});The client is generated from the newest version of the spec. Properties use camelCase (issue.createdOn), the client converts them from and to the snake_case of the API.
Known limitations of the generated client (see the it.fails tests):
- Query parameters with an empty string value are not sent, so
all_words=""(search for any word) andstatus=""(users of all statuses) can't be used. - Attachment downloads and thumbnails can't be read through the client, use the
content_urlof the attachment instead. - Lists are paginated with
offsetandlimitonly: Redmine has no "next" link, so the pager returns a single page. Seedev/index.tsfor iterating over all pages.
To try the client against your own server, create dev/.env with REDMINE_URL and REDMINE_API_KEY, then run pnpm dev.
pnpm build # the tests use the generated client and OpenAPI documents
pnpm test # runs against the latest Redmine 7 imageThe global setup (tests/setup/global-setup.ts) starts Redmine in Docker, seeds it with seed.rb and removes the container afterwards. Each test calls the API through the generated client and validates the raw response against the OpenAPI document of the Redmine version under test, using ajv. A test fails when a response doesn't match the spec, uses an undeclared status code, or contains properties the spec doesn't declare.
The tests run as a regular user (tester), which creates its own projects. An admin client is only used for admin-only endpoints (users, groups, custom fields, archiving projects), setup and cleanup.
The hand-written code in packages/ has unit tests (vitest) that answer requests with a fake HTTP client, so they don't need Docker. They also check the types with tsc.
pnpm build # the React package tests use the built client
pnpm test:packagesEach supported release series has a script in tests/package.json:
cd tests
pnpm run test:redmine-4.2Any image can be tested with REDMINE_IMAGE, e.g. REDMINE_IMAGE=redmine:6.0.11 pnpm test. Operations the spec doesn't include for that version are skipped. Each version runs in its own Docker Compose project on a free port, so several versions can run in parallel.
Redmine 3.0 can't be tested: its images use a manifest format that current Docker versions can't pull.
| Variable | Effect |
|---|---|
REDMINE_IMAGE |
Docker image to test, defaults to redmine:7 |
REDMINE_PORT |
Fixed host port instead of a free one |
KEEP_REDMINE=1 |
Keep the container after the tests, and reuse it on the next run |
Tests live in tests/specs/api/, one file per resource. Helpers in tests/helpers/:
describeOperation("Issues_getIssue", ...): adescribeblock for one operation, skipped when the operation is not in the spec for the server's version.withResponse(operationId): captures the raw response of a client call,expectValid()validates it against the spec.expectHttpError(promise, status, operationId): expects an error response and validates it.expectPagination(...): checksoffset,limitandtotal_countof a paginated list.fixtures.ts: create projects, issues, users, news, wiki pages, ...
describeOperation("Issues_getIssue", () => {
it("gets an issue", async () => {
const issue = await createIssue(client, project.id);
const v = withResponse("Issues_getIssue");
const { issue: fetched } = await client.issuesClient.getIssue(issue.id, v.options);
v.expectValid();
expect(fetched.id).toBe(issue.id);
});
});Use spec.isAtLeast("4.1.0") for checks that depend on the Redmine version.
Releases are made by semantic-release in the release workflow, on every push to main or beta. The next version follows from the Conventional Commits since the last release: fix: makes a patch, feat: a minor and a ! or BREAKING CHANGE: footer a major release. Other types (docs:, build:, test:, ...) appear in the release notes but don't trigger a release.
| Branch | Version | npm dist-tag | GitHub release |
|---|---|---|---|
main |
0.1.0 |
latest |
release |
beta |
0.1.0-beta.1 |
beta |
prerelease |
A release (release.config.mjs):
- Sets the version of the root package,
specand the packages inpackages/withpnpm version:packages, so they always share one version. - Builds the packages and publishes them to npm with
pnpm publish:packages --provenance, which replaces theworkspace:ranges with the released version. - Commits the bumped
package.jsonfiles andCHANGELOG.mdaschore(release): <version> [skip ci], tags it and creates the GitHub release.
npm authenticates the workflow by trusted publishing (OIDC), so no npm token is stored. GitHub uses the workflow's GITHUB_TOKEN.
To check what the next release would be, without publishing anything:
pnpm release --dry-run --no-ci