Skip to content

A DICOM server built with Bun that supports both traditional DIMSE protocol (limited) and DICOMweb (REST) APIs. Also includes NIfTIweb routes for managing NIfTI files commonly used in research and non-clinical settings.

License

Notifications You must be signed in to change notification settings

hanayik/dicomweb

dicomweb

Note: until version 1.0.0, this software should be treated as alpha software, and changes will happen rapidly to get to a stable version 1.0.0

A DICOM server built with Bun that supports both traditional DIMSE protocol and DICOMweb (REST) APIs. Also includes NIfTIweb routes for managing NIfTI files commonly used in research and non-clinical settings.

DICOM: Receives DICOM images via C-STORE or STOW-RS and provides QIDO-RS/WADO-RS for querying and retrieval.

NIfTI: Accepts NIfTI files (.nii/.nii.gz) with user-supplied metadata and provides similar query/retrieve/thumbnail functionality.

Prerequisites

Install Bun:

curl -fsSL https://bun.sh/install | bash

Install

bun install

Run

# Start the dev server
bun run dev

Build

# outputs a single-file-executable in ./dist/dicomweb-server
bun run build

Test

bun test

Tests start the server and run integration tests against both DIMSE and HTTP/DICOMweb protocols using the sample data in test-data/.

Note: test-data/ is not included in the git repo (yet).

Organization

src/
├── index.ts          # Entry point
├── config.ts         # Environment configuration
├── db/               # SQLite database (DICOM + NIfTI tables)
├── dicom/            # DIMSE protocol handling (C-ECHO, C-STORE)
└── http/
    ├── dicomweb/     # DICOMweb REST API (QIDO-RS, WADO-RS, STOW-RS)
    └── niftiweb/     # NIfTI REST API (Query, Retrieve, Store, Thumbnails)

Building a Standalone Executable

bun run build

This creates a single dicomweb-server executable that can be distributed without requiring Bun or Node.js to be installed.

WebAssembly Embedding for Compiled Executables

The dcmjs-imaging library uses WebAssembly for DICOM image decoding (JPEG, JPEG 2000, etc.). When running with bun run dev, the library loads the .wasm file from node_modules using Node.js's fs module (via Bun's Node API). However, compiled Bun executables cannot use require('fs') - attempting to do so results in:

ReferenceError: require is not defined

To solve this, the WebAssembly module is embedded directly into the executable using src/wasm-loader.ts:

How it works:

  1. Embed the wasm file using Bun's type: "file" import attribute:

    import wasmPath from "../node_modules/dcmjs-imaging/build/dcmjs-native-codecs.wasm" with { type: "file" };

    This embeds the file into the executable and returns a virtual path that Bun.file() can read.

  2. Load at startup using Bun.file() which works with Bun's virtual filesystem ($bunfs):

    const file = Bun.file(wasmPath);
    const arrayBuffer = await file.arrayBuffer();
    wasmBuffer = Buffer.from(arrayBuffer);
  3. Shim Node.js modules that dcmjs-imaging expects (fs and path):

    globalThis.require = (moduleName: string) => {
      if (moduleName === "fs") {
        return {
          promises: {
            readFile: async (path) => wasmBuffer  // Return embedded bytes
          }
        };
      }
      // ... handle 'path' module
    };
  4. Import the loader first in src/index.ts to ensure shims are set up before dcmjs-imaging loads:

    import "./wasm-loader";  // Must be first!

DICOMweb API

QIDO-RS (Query)

# Search studies
curl http://localhost:3000/dicomweb/studies

# Search with filters
curl "http://localhost:3000/dicomweb/studies?PatientName=DOE*&Modality=MR"

# Search series in a study
curl http://localhost:3000/dicomweb/studies/{studyUID}/series

# Search instances in a series
curl http://localhost:3000/dicomweb/studies/{studyUID}/series/{seriesUID}/instances

WADO-RS (Retrieve)

# Retrieve study (multipart DICOM)
curl -H "Accept: multipart/related; type=application/dicom" \
  http://localhost:3000/dicomweb/studies/{studyUID}

# Retrieve instance metadata
curl http://localhost:3000/dicomweb/studies/{studyUID}/series/{seriesUID}/instances/{sopUID}/metadata

# Retrieve rendered image (JPEG/PNG)
curl -H "Accept: image/jpeg" \
  http://localhost:3000/dicomweb/studies/{studyUID}/series/{seriesUID}/instances/{sopUID}/rendered

# Retrieve thumbnails (scaled images)
curl "http://localhost:3000/dicomweb/studies/{studyUID}/thumbnail?viewport=100,100"
curl "http://localhost:3000/dicomweb/studies/{studyUID}/series/{seriesUID}/thumbnail?viewport=100,100"

STOW-RS (Store)

# Store DICOM files via HTTP
curl -X POST \
  -H "Content-Type: multipart/related; type=application/dicom; boundary=myboundary" \
  --data-binary @multipart-body.bin \
  http://localhost:3000/dicomweb/studies

NIfTIweb API

NIfTI files use a simpler hierarchy: Study → Series (one NIfTI file per series). Since NIfTI files don't contain DICOM-style metadata, you supply metadata during upload.

Upload (Store)

# Upload a NIfTI file with metadata
curl -X POST \
  -F 'metadata={"patientId":"PAT001","patientName":"Doe^John","studyDescription":"Brain MRI","modality":"MR"}' \
  -F 'file=@brain.nii.gz' \
  http://localhost:3000/niftiweb/studies

# Upload to existing study
curl -X POST \
  -F 'metadata={"seriesDescription":"T1 MPRAGE"}' \
  -F 'file=@t1.nii.gz' \
  http://localhost:3000/niftiweb/studies/{studyUID}

Metadata fields (all optional, UIDs auto-generated if not provided):

  • studyUID, seriesUID - Identifiers
  • patientId, patientName - Patient info
  • studyDate, studyDescription, accessionNumber - Study info
  • modality, seriesNumber, seriesDescription - Series info

Query (Search)

# Search studies
curl http://localhost:3000/niftiweb/studies

# Search with filters
curl "http://localhost:3000/niftiweb/studies?PatientName=Doe*&Modality=MR"

# Search series in a study
curl http://localhost:3000/niftiweb/studies/{studyUID}/series

Retrieve

# Get study metadata
curl http://localhost:3000/niftiweb/studies/{studyUID}/metadata

# Get series metadata (includes dimensions, voxel sizes)
curl http://localhost:3000/niftiweb/studies/{studyUID}/series/{seriesUID}/metadata

# Download NIfTI file
curl -o volume.nii.gz \
  http://localhost:3000/niftiweb/studies/{studyUID}/series/{seriesUID}

Thumbnails & Rendered Slices

# Study thumbnail (middle slice of middle series)
curl http://localhost:3000/niftiweb/studies/{studyUID}/thumbnail -o thumb.jpg

# Series thumbnail with viewport
curl "http://localhost:3000/niftiweb/studies/{studyUID}/series/{seriesUID}/thumbnail?viewport=128,128" -o thumb.jpg

# Rendered slice with plane and slice selection
curl "http://localhost:3000/niftiweb/studies/{studyUID}/series/{seriesUID}/rendered?plane=axial&slice=50" -o slice.jpg

# Available planes: axial, coronal, sagittal (auto-selects highest resolution if omitted)
# Slice defaults to middle slice if not specified

DIMSE Operations

# Test connectivity (C-ECHO)
echoscu -aec DICOM_SCP localhost 11112

# Send DICOM files (C-STORE)
storescu -aec DICOM_SCP localhost 11112 /path/to/file.dcm

Development Scripts

DICOMweb Client Scripts

# Health check
bun run scripts/dicomweb-health.ts

# Search (QIDO-RS)
bun run scripts/dicomweb-search.ts studies
bun run scripts/dicomweb-search.ts studies --PatientName=SOMEONE
bun run scripts/dicomweb-search.ts studies --PatientName=SOMEONE* --Modality=MR
bun run scripts/dicomweb-search.ts series {studyUID}
bun run scripts/dicomweb-search.ts instances {studyUID} {seriesUID}

# Retrieve (WADO-RS)
bun run scripts/dicomweb-retrieve.ts study {studyUID}
bun run scripts/dicomweb-retrieve.ts instance {studyUID} {seriesUID} {sopUID} --format=metadata
bun run scripts/dicomweb-retrieve.ts instance {studyUID} {seriesUID} {sopUID} --format=rendered

# Store (STOW-RS)
bun run scripts/dicomweb-store.ts ./image.dcm
bun run scripts/dicomweb-store.ts ./dicom-folder/

# Thumbnails
bun run scripts/dicomweb-thumbnail.ts study {studyUID}
bun run scripts/dicomweb-thumbnail.ts study {studyUID} --viewport=100,100
bun run scripts/dicomweb-thumbnail.ts series {studyUID} {seriesUID} --viewport=200,200
bun run scripts/dicomweb-thumbnail.ts instance {studyUID} {seriesUID} {sopUID}
bun run scripts/dicomweb-thumbnail.ts frame {studyUID} {seriesUID} {sopUID} 1 --viewport=64,64

DIMSE Client Scripts

# C-ECHO test
bun run scripts/echo-scu.ts

# C-STORE files
bun run scripts/store-scu.ts ./image.dcm
bun run scripts/store-scu.ts ./dicom-folder/

Configuration

Variable Default Description
DICOM_PORT 11112 DIMSE listener port
HTTP_PORT 3000 REST API port
AE_TITLE DICOM_SCP Application Entity title
RESET_DB false Set to "true" to delete and recreate database on startup
DICOMWEB_URL http://localhost:3000/dicomweb Base URL for scripts
OUTPUT_DIR ./retrieved Output directory for retrieved files

OHIF Viewer Integration

To use this server with OHIF Viewer, add the following data source to your OHIF configuration:

dataSources: [
  {
    namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
    sourceName: 'virdx',
    configuration: {
      friendlyName: 'VIRDX DICOMweb server',
      name: 'virdx',
      // update based on actual deployment
      wadoUriRoot: 'http://localhost:3000/dicomweb',
      // update based on actual deployment
      qidoRoot: 'http://localhost:3000/dicomweb',
      // update based on actual deployment
      wadoRoot: 'http://localhost:3000/dicomweb',
      qidoSupportsIncludeField: true,
      imageRendering: 'wadors',
      thumbnailRendering: 'wadors',
      enableStudyLazyLoad: true,
      supportsFuzzyMatching: false,
      supportsWildcard: true,
      staticWado: true,
      bulkDataURI: {
        enabled: false,
        relativeResolution: 'studies',
      },
      omitQuotationForMultipartRequest: true,
    },
  },
  // ... other data sources
]

DICOMweb Endpoints

Endpoint Method Service Description
/dicomweb/studies GET QIDO-RS Search studies
/dicomweb/studies POST STOW-RS Store instances
/dicomweb/studies/{uid}/series GET QIDO-RS Search series
/dicomweb/studies/{uid}/series/{uid}/instances GET QIDO-RS Search instances
/dicomweb/studies/{uid} GET WADO-RS Retrieve study
/dicomweb/studies/{uid}/metadata GET WADO-RS Study metadata
/dicomweb/studies/{uid}/series/{uid} GET WADO-RS Retrieve series
/dicomweb/studies/{uid}/series/{uid}/instances/{uid} GET WADO-RS Retrieve instance
/dicomweb/studies/{uid}/series/{uid}/instances/{uid}/metadata GET WADO-RS Instance metadata
/dicomweb/studies/{uid}/series/{uid}/instances/{uid}/rendered GET WADO-RS Rendered image
/dicomweb/studies/{uid}/thumbnail GET WADO-RS Study thumbnail
/dicomweb/studies/{uid}/series/{uid}/thumbnail GET WADO-RS Series thumbnail
/dicomweb/studies/{uid}/series/{uid}/instances/{uid}/thumbnail GET WADO-RS Instance thumbnail
/dicomweb/studies/{uid}/series/{uid}/instances/{uid}/frames/{frame}/thumbnail GET WADO-RS Frame thumbnail

NIfTIweb Endpoints

Endpoint Method Description
/niftiweb GET Health check
/niftiweb/studies GET Search studies
/niftiweb/studies POST Upload NIfTI file
/niftiweb/studies/{studyUID} GET Get study info
/niftiweb/studies/{studyUID} POST Upload to existing study
/niftiweb/studies/{studyUID}/series GET Search series
/niftiweb/studies/{studyUID}/metadata GET Study metadata (JSON)
/niftiweb/studies/{studyUID}/thumbnail GET Study thumbnail
/niftiweb/studies/{studyUID}/series/{seriesUID} GET Download NIfTI file
/niftiweb/studies/{studyUID}/series/{seriesUID}/metadata GET Series metadata (JSON)
/niftiweb/studies/{studyUID}/series/{seriesUID}/thumbnail GET Series thumbnail
/niftiweb/studies/{studyUID}/series/{seriesUID}/rendered GET Rendered slice

Query parameters for thumbnails/rendered:

  • plane - axial, coronal, or sagittal (auto-selects highest resolution)
  • slice - Slice index (defaults to middle)
  • viewport - Dimensions like 256,256

Load Testing

This is a very basic way to load test the dicomweb server. Something more comprehensive can be used in the future.

Note: there is no cleanup implemented until the DELETE method is supported.

# Start server with fresh database before each load test
RESET_DB=true bun run dev

# Basic load test (uploads test data, runs 5 test types, cleans up)
bun run scripts/load-test.ts

# Custom workers and iterations
bun run scripts/load-test.ts --workers=8 --iterations=500

# Mixed workload (all test types run concurrently - more realistic)
bun run scripts/load-test.ts --workers=8 --iterations=500 --mixed

# Keep data after test for inspection
bun run scripts/load-test.ts --skip-cleanup

The load test uploads DICOM files from test-data/T1-brain/, then benchmarks QIDO-RS (search) and WADO-RS (retrieve) endpoints.

Acknowledgments

This project relies on several excellent open-source tools and packages:

Runtime:

  • Bun - Fast JavaScript runtime with built-in SQLite, HTTP server, and single-file executable compilation

DICOM:

  • dcmjs - DICOM data parsing and manipulation
  • dcmjs-dimse - DIMSE protocol implementation (C-ECHO, C-STORE handling)
  • dcmjs-imaging - Pixel data decoding for rendered image output

NIfTI:

Image Processing:

  • sharp - High-performance image format conversion (RGBA to JPEG/PNG)

About

A DICOM server built with Bun that supports both traditional DIMSE protocol (limited) and DICOMweb (REST) APIs. Also includes NIfTIweb routes for managing NIfTI files commonly used in research and non-clinical settings.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published