How to wire IBM’s @docling/docling-core, @docling/docling-components, and a Python AI backend into a single, coherent React + Node.js application — with a fully offline path that needs no server at all.
Disclaimer: This text and corpus were created using IBM Bob in “Story Telling” ad-hoc mode (on purpose) — killing two birds with one stone!
*It showcases the power of docling-ts while giving you a glimpse into another one of Bob’s many capabilities.
*
First things first: what is Docling-TS?
Docling TS is a set of Type/JavaScript libraries and examples for working with the JSON output format of Docling.
Note: “Docling TS” is an unstable draft implementation that evolves quickly.
Introduction
Document intelligence — the ability to understand the structure and semantics of a PDF, DOCX, spreadsheet, or image — has historically been the exclusive domain of Python. You needed heavy ML models, complex pipelines, and significant infrastructure just to extract a table from a scanned report.
[Docling](Introduction
Document intelligence — the ability to understand the structure and semantics of a PDF, DOCX, spreadsheet, or image — has historically been the exclusive domain of Python. You needed heavy ML models, complex pipelines, and significant infrastructure just to extract a table from a scanned report.
Docling, an open-source project by IBM Research, changes that equation. It provides a best-in-class Python AI engine for document parsing, but critically it also ships a TypeScript ecosystem — @docling/docling-core and @docling/docling-components — that lets front-end and full-stack developers consume pre-converted documents entirely in the browser, with zero Python dependency.
This blog post walks through Docling TS Demo, a full-stack demonstration application that showcases three distinct usage modes:), an open-source project by IBM Research, changes that equation. It provides a best-in-class Python AI engine for document parsing, but critically it also ships a TypeScript ecosystem — @docling/docling-core and @docling/docling-components — that lets front-end and full-stack developers consume pre-converted documents entirely in the browser, with zero Python dependency.
This blog post walks through Docling TS Demo, a full-stack demonstration application that showcases three distinct usage modes:
| Mode | Server? | What it uses |
| --------------------- | --------------- | ------------------------------------------------------------ |
| 🟢 **JSON Viewer** | ❌ None | `@docling/docling-core` + `@docling/docling-components` — pure browser TypeScript |
| 🟢 **SDK Explorer** | ❌ None | Copy-ready code for all Docling TS packages |
| 🔵 **Live Conversion** | ✅ docling-serve | Express proxy → Python AI backend |
The key insight: docling-ts does not convert documents. It provides the TypeScript layer to consume pre-converted Docling JSON. Convert once with the Python CLI, then work entirely offline in TypeScript — no servers, no models, no latency.
Architecture
The application is built with three distinct layers that can operate fully independently.
System Architecture — Three Modes
Offline Data Flow (JSON Viewer)
The offline path is the heart of the docling-ts story. A document is converted once with the Python CLI, and thereafter the TypeScript layer takes over entirely inside the browser.
Live Conversion Data Flow
When docling-serve is running, the full AI pipeline becomes available. Files are forwarded from the React UI through an Express proxy to the Python backend, and the results — Markdown, HTML, JSON, plain text — are persisted to disk and returned to the browser in a single response.
Package Dependency Map
Mode Decision Tree
Project Structure
docling-ts-demo/
├── src/
│ ├── server/ # Express API (optional proxy)
│ │ ├── index.ts # Server bootstrap + route wiring
│ │ └── routes/
│ │ ├── convert.ts # /api/convert/file + /api/convert/url
│ │ ├── health.ts # /api/health
│ │ └── doclingInfo.ts # /api/docling/info
│ ├── client/
│ │ └── src/
│ │ ├── App.tsx # Router + sidebar layout
│ │ ├── pages/
│ │ │ ├── JsonViewerPage.tsx ← offline docling-ts demo
│ │ │ ├── HomePage.tsx ← 3-mode overview
│ │ │ ├── ConvertPage.tsx ← live file upload
│ │ │ ├── UrlConvertPage.tsx ← live URL convert
│ │ │ ├── ComponentsPage.tsx ← SDK code explorer
│ │ │ └── HealthPage.tsx ← system status
│ │ └── types/
│ │ └── docling.d.ts ← JSX types for web components
│ └── tests/test.ts # 11 integration tests
├── input/
│ ├── sample-document.md
│ └── sample-docling.json ← ready-to-use offline demo file
├── scripts/
│ ├── start.sh # Launch in detached mode
│ └── stop.sh # Graceful shutdown
├── Docs/
│ ├── Architecture.md
│ └── Quickstart.md
└── README.md
Code Deep-Dive
The Offline JSON Viewer — @docling/docling-core in Action
The centrepiece of the application is JsonViewerPage.tsx, which demonstrates the full offline path with zero server calls.
-
Importing real types and utilities:
DoclingDocumentis the top-level TypeScript type that mirrors the PythonDoclingDocumentPydantic model. iterateDocumentItems is a generator that walks the document body in reading order, andisDoclingprovides discriminated type guards —isDocling.TextItem(item),isDocling.TableItem(item), etc.
// Real imports from @docling/docling-core
import {
type DoclingDocument,
type SectionHeaderItem,
type TextItem,
type TableItem,
type PageItem,
iterateDocumentItems,
isDocling,
} from '@docling/docling-core'
-
Type-safe document analysis: This is purely in-browser TypeScript — no fetch, no server calls, just
JSON.parseinto a typed structure.
function analyseDocument(doc: DoclingDocument): DocStats {
const pages = doc.pages ? Object.values(doc.pages) : []
const headers: SectionHeaderItem[] = []
const textItems: TextItem[] = []
const tables: TableItem[] = []
let pictureCount = 0
let totalItems = 0
for (const [item] of iterateDocumentItems(doc)) {
totalItems++
if (isDocling.SectionHeaderItem(item)) headers.push(item)
else if (isDocling.TextItem(item)) textItems.push(item)
else if (isDocling.TableItem(item)) tables.push(item)
else if (isDocling.PictureItem(item)) pictureCount++
}
return { pages, headers, textItems, tables, pictureCount, totalItems }
}
-
Lazy-loading
@docling/docling-componentsweb components: The web components are loaded as a dynamic import to avoid bundling them unless the user actually opens the viewer tab. Once loaded,<docling-img>and<docling-table>are globally registered as custom HTML elements.
// Lazy-load @docling/docling-components — registers custom elements globally
useEffect(() => {
import('@docling/docling-components')
.then(() => setComponentsLoaded(true))
.catch(() => setComponentsLoaded(false))
}, [])
-
Wiring a web component via a React ref: Because
<docling-img>is a custom element, React cannot set complex objects via JSX attributes alone. We use arefand set the.srcproperty programmatically after mount — a clean pattern for bridging React and native Web Components.
function DoclingImgViewer({ doc }: { doc: DoclingDocument }) {
const ref = useRef<HTMLElement>(null)
useEffect(() => {
if (ref.current) {
// The web component accepts a parsed DoclingDocument object on `.src`
;(ref.current as unknown as { src: unknown }).src = doc
}
}, [doc])
return (
<docling-img ref={ref} pagenumbers="" style={{ display: 'block', width: '100%' }} />
)
}
-
File ingestion via FileReader: No network round-trip. The entire pipeline from file drop to typed
DoclingDocumentis local and synchronous.
const onDrop = useCallback((accepted: File[]) => {
const file = accepted[0]
if (!file) return
const reader = new FileReader()
reader.onload = (e) => {
try {
const parsed = JSON.parse(e.target?.result as string) as DoclingDocument
setDoc(parsed)
setFilename(file.name)
setActiveTab('stats')
} catch {
setError('Could not parse file as a Docling JSON document.')
}
}
reader.readAsText(file)
}, [])
The Express Backend — Proxy to docling-serve
The backend, defined in src/server/index.ts, is intentionally thin. Its sole responsibility for live conversion is to act as a secure, CORS-enabled proxy that adds API key support and persists outputs to disk.
Server bootstrap
const app = express()
const PORT = parseInt(process.env.PORT || '3001', 10)
app.use(cors())
app.use(express.json({ limit: '50mb' }))
app.use('/output', express.static(OUTPUT_DIR))
app.use('/api/health', healthRouter)
app.use('/api/convert', convertRouter)
app.use('/api/docling', doclingInfoRouter)
File conversion endpoint — forwarding to docling-serve
The convert.ts route receives a multipart file upload, builds a new FormData for docling-serve, and persists every output format with a timestamp:
router.post(
'/file',
upload.single('file'),
async (req: Request, res: Response): Promise<void> => {
// Build multipart form for docling-serve
const form = new FormData()
form.append('files', req.file.buffer, {
filename: req.file.originalname,
contentType: req.file.mimetype,
})
outputFormats.forEach((fmt) => form.append('to_formats', fmt))
form.append('do_table_structure', 'true')
const response = await axios.post(
`${doclingUrl}/v1/convert/file`,
form,
{ headers, timeout: 300000 } // 5 min timeout for large docs
)
// Persist outputs to output/ with a timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
if (docData.document?.md_content) {
const outPath = path.join(outputDir, `${baseName}_${timestamp}.md`)
fs.writeFileSync(outPath, docData.document.md_content, 'utf-8')
savedFiles.markdown = `/output/${path.basename(outPath)}`
}
// …same for html, json
}
)
The URL conversion endpoint follows an identical pattern, pointing at /v1/convert/source and forwarding http_sources:
await axios.post(
`${doclingUrl}/v1/convert/source`,
{
http_sources: [{ url }],
options: { to_formats: toFormats, do_table_structure: true },
},
{ headers, timeout: 300000 }
)
The SDK Explorer — Copy-Ready Code Examples
ComponentsPage.tsx demonstrates how to use all three packages through a tabbed UI of copy-ready snippets. Here is the @docling/docling-core example embedded in the component:
// Iterate all items with type discrimination
import { iterateDocumentItems, isDocling } from '@docling/docling-core'
const doc = await fetchDoc('https://your-host/conversion.json')
for (const [item, level] of iterateDocumentItems(doc)) {
if (isDocling.TextItem(item)) {
console.log('TEXT:', item.text)
} else if (isDocling.TableItem(item)) {
console.log('TABLE cells:', item.data.grid.length)
} else if (isDocling.PictureItem(item)) {
console.log('PICTURE at page:', item.prov?.[0]?.page_no)
} else if (isDocling.SectionHeaderItem(item)) {
console.log('HEADER (level', level, '):', item.text)
}
}
And the docling-sdk community client pattern for RAG pipelines:
const client = new Docling({
api: { baseUrl: process.env.DOCLING_SERVE_URL ?? 'http://localhost:5001' },
})
// Document chunking (for RAG pipelines)
const chunks = await client.chunkHybridSync(buf, 'document.pdf', {
chunking_max_tokens: 200,
})
chunks.forEach(c => console.log(c.text))
Integration Tests — Offline + Live Suites
The test suite in src/tests/test.ts covers four suites across 11 tests. Suites 1–3 always run without docling-serve; Suite 4 auto-skips gracefully if the Python backend is unreachable.
// Suite 3 — Offline JSON parsing (no server needed)
await test('sample-docling.json has DoclingDocument structure (pages + texts + tables)', async () => {
const doc = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (typeof doc.pages !== 'object') throw new Error('missing pages')
if (!Array.isArray(doc.texts)) throw new Error('missing texts array')
if (!Array.isArray(doc.tables)) throw new Error('missing tables array')
})
// Suite 4 — graceful skip if docling-serve is not reachable
if (!doclingAvailable) {
console.log(' ⚠️ Docling Serve not reachable — skipping live conversion tests (this is fine)')
}
This design makes CI/CD straightforward: the core test battery never requires the AI backend.
Application Router
The App.tsx root component wires the sidebar navigation and all page routes in a clean BrowserRouter + Routes structure:
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/json-viewer" element={<JsonViewerPage />} />
<Route path="/convert" element={<ConvertPage />} />
<Route path="/url-convert" element={<UrlConvertPage />} />
<Route path="/components" element={<ComponentsPage />} />
<Route path="/health" element={<HealthPage />} />
</Routes>
The sidebar distinguishes the two modes visually with emoji badges, making the offline vs. live distinction obvious to users at a glance.
Key Dependencies
| Package | Version | Role |
| ----------------------------- | -------- | ------------------------------------------------------------ |
| `@docling/docling-core` | `^0.0.7` | TypeScript types + `iterateDocumentItems` / `isDocling` utilities |
| `@docling/docling-components` | `^0.0.7` | `<docling-img>` / `<docling-table>` web components |
| `react` + `vite` | 18 / 5 | Frontend SPA with HMR |
| `express` | 4 | Backend API proxy (optional) |
| `multer` + `axios` | — | File upload + HTTP forwarding |
| `react-dropzone` | 14 | Drag-and-drop file zones |
| `docling-sdk` | `^2.0.0` | Community TypeScript API/CLI/Web client |
Getting Started in 3 Steps
# 1. Clone and configure
cp .env.example .env
# 2. Start the demo (offline features work immediately)
./scripts/start.sh
# → http://localhost:3000
# 3. (Optional) Start docling-serve for live conversion
podman run -p 5001:5001 \
-e DOCLING_SERVE_ENABLE_UI=1 \
quay.io/docling-project/docling-serve
Drop input/sample-docling.json onto the JSON Viewer page to explore the offline mode immediately — no Python, no container, no models required.
Conclusion
The Docling TS Demo project demonstrates that document intelligence no longer has to be a Python-only story.
Through this application we can demonstrate end-to-end;
A fully offline document viewer —
@docling/docling-coreprovides production-quality TypeScript types (DoclingDocument,TextItem,TableItem,PictureItem,SectionHeaderItem) and theiterateDocumentItems / isDoclingutility layer that mirrors the Python library's API. Any document converted once by the Docling Python CLI can be explored, parsed, and rendered in a React application with zero server dependencies.Native web component integration —
@docling/docling-components ships<docling-img>and<docling-table>as standards-based custom elements. Bridging them into a React application viarefsand programmatic property assignment (el.src = doc) is a clean, future-proof pattern that works today with any modern browser.A production-ready Express proxy — for teams that need live, on-demand conversion, the three-tier architecture (React → Express → docling-serve) provides a clean separation of concerns. The API key layer, memory-buffered uploads, 5-minute timeout, and timestamped output persistence are all patterns directly applicable to production workloads.
A test strategy that matches the architecture — the four-suite integration test battery (API health, input validation, offline JSON parsing, optional live conversion) reflects the tiered nature of the system. CI always passes on the offline suites; the live suite degrades gracefully when the AI backend is absent.
Developer experience — the SDK Explorer page, copy-ready code snippets, and the mode decision tree lower the barrier to entry for developers who want to add document intelligence to TypeScript applications without committing to a full ML infrastructure.
The Docling TypeScript ecosystem is young — both packages are at v0.0.7 — but the design mirrors the mature Python library closely enough that the API surface will be familiar to anyone who has used docling-core in Python. This project serves as a concrete, working reference for anyone looking to bring structured document understanding into the browser.
Thanks for reading (I wrote this 😂)
Links
- Github repository for this post: https://github.com/aairom/docling-ts-demo
- Docling: https://docling.ai/
- Docling-ts: https://github.com/docling-project/docling-ts









Top comments (0)