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.
Install Bun:
curl -fsSL https://bun.sh/install | bashbun install# Start the dev server
bun run dev# outputs a single-file-executable in ./dist/dicomweb-server
bun run buildbun testTests 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).
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)
bun run buildThis creates a single dicomweb-server executable that can be distributed without requiring Bun or Node.js to be installed.
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:
-
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. -
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);
-
Shim Node.js modules that
dcmjs-imagingexpects (fsandpath):globalThis.require = (moduleName: string) => { if (moduleName === "fs") { return { promises: { readFile: async (path) => wasmBuffer // Return embedded bytes } }; } // ... handle 'path' module };
-
Import the loader first in
src/index.tsto ensure shims are set up beforedcmjs-imagingloads:import "./wasm-loader"; // Must be first!
# 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# 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"# 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/studiesNIfTI 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 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- IdentifierspatientId,patientName- Patient infostudyDate,studyDescription,accessionNumber- Study infomodality,seriesNumber,seriesDescription- Series info
# 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# 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}# 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# 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# 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# 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/| 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 |
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
]| 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 |
| 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, orsagittal(auto-selects highest resolution)slice- Slice index (defaults to middle)viewport- Dimensions like256,256
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-cleanupThe load test uploads DICOM files from test-data/T1-brain/, then benchmarks QIDO-RS (search) and WADO-RS (retrieve) endpoints.
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:
- nifti-reader-js - NIfTI file parsing and reading
Image Processing:
- sharp - High-performance image format conversion (RGBA to JPEG/PNG)