DEV Community

Alain Airom (Ayrom)
Alain Airom (Ayrom)

Posted on

Building a Full-Stack Document Intelligence with the Docling TypeScript Ecosystem

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: DoclingDocument is the top-level TypeScript type that mirrors the Python DoclingDocument Pydantic model. iterateDocumentItems is a generator that walks the document body in reading order, and isDocling provides 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'
Enter fullscreen mode Exit fullscreen mode
  • Type-safe document analysis: This is purely in-browser TypeScript — no fetch, no server calls, just JSON.parse into 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 }
}
Enter fullscreen mode Exit fullscreen mode
  • Lazy-loading @docling/docling-components web 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))
}, [])
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 a ref and set the .src property 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 DoclingDocument is 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)
}, [])
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 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)
  }
}
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 reachable
if (!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 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
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;

  1. A fully offline document viewer@docling/docling-core provides production-quality TypeScript types (DoclingDocument, TextItem, TableItem, PictureItem, SectionHeaderItem) and the iterateDocumentItems / isDocling utility 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.

  2. Native web component integration@docling/docling-components ships <docling-img> and <docling-table> as standards-based custom elements. Bridging them into a React application via refs and programmatic property assignment (el.src = doc) is a clean, future-proof pattern that works today with any modern browser.

  3. 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.

  4. 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.

  5. 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

Top comments (0)