
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 |
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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-coreimport {type DoclingDocument,type SectionHeaderItem,type TextItem,type TableItem,type PageItem,iterateDocumentItems,isDocling,} from '@docling/docling-core'Enter fullscreen mode Exit fullscreen mode
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 = 0let 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 }}Enter fullscreen mode Exit fullscreen mode
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 globallyuseEffect(() => {import('@docling/docling-components')
.then(() => setComponentsLoaded(true))
.catch(() => setComponentsLoaded(false))}, [])Enter fullscreen mode Exit fullscreen mode
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%' }} />
)}Enter fullscreen mode Exit fullscreen mode
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) returnconst 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)}, [])Enter fullscreen mode Exit fullscreen mode
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)Enter fullscreen mode Exit fullscreen mode
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})Enter fullscreen mode Exit fullscreen mode
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 })Enter fullscreen mode Exit fullscreen mode
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 discriminationimport { 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)}}Enter fullscreen mode Exit fullscreen mode
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))Enter fullscreen mode Exit fullscreen mode
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 reachableif (!doclingAvailable) {console.log(' ⚠️ Docling Serve not reachable — skipping live conversion tests (this is fine)')}Enter fullscreen mode Exit fullscreen mode
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>
Enter fullscreen mode Exit fullscreen mode
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 |
Enter fullscreen mode Exit fullscreen mode
Getting Started in 3 Steps
# 1. Clone and configurecp .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
Enter fullscreen mode Exit fullscreen mode

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
Comments (0)
Login to post a comment.