ZyVOP Logo
Content That Connects
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZyVOP Logo
Content That Connects

The Developer Publishing Hub. Write once, cross-post to Dev.to, Medium, Hashnode, WordPress & Bluesky with automated canonical source tags and zero paywalls.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • Developer API & CLI
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Developer Publishing Hub.

Zero paywalls · Full content ownership
All systems operational
HomeBuilding a Full-Stack Document Intelligence with the Docling TypeScript Ecosystem

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

Alain Airom (Ayrom)
Alain Airom (Ayrom)
Build Engineer
August 31, 2026
9 min read
Building a Full-Stack Document Intelligence with the Docling TypeScript Ecosystem
#bob#doclingts#docling
👍1

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-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.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 = 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-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 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 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) 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;

  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

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

Alain Airom (Ayrom)
Alain Airom (Ayrom)

Build Engineer

IT guy, IBMer... sharing my hands-on experiences and technical subjects of my interest (IBM or not). A bit "touche à tout"!

Subscribe to Alain Airom (Ayrom)'s Newsletter

More from Alain Airom (Ayrom)

View profile

Automating AI Context: How I Built a Custom Extension for IBM Bob IDE to Inject Project Rules

Building my own “vsix” extension to automate my own projects Introduction Setting up consistent workspace instructions across projects can quickly become a tedi...

10 minAug 31

Deep Dive: Testing Radar UI for Kubernetes using MCP, a Go GUI, and an Autonomous Agent

Hand-on test of radarhq.io K8S UI and dashboard on a macOS with Minikube and Podman Introduction Kubernetes dashboards are often either overloaded with unnecess...

5 minAug 31

Embedding Docling-NLP in Ad-Hoc UI Applications: A Lightweight Blueprint

Implementing a “Graph Language Model” with Docling-NLP Image from Docling-AI Building a local, privacy-focused Document AI pipeline often feels like balancing h...

3 minAug 31

Loop Engineering 101

From Curiosity to Creation: Building a Loop Engineering Application with IBM’s Bob Introduction For some time now, “Loop Engineering” has been generating signif...

9 minAug 31

Building an OpenTelemetry Instrumentation Wizard

Accelerating observability adoption by automating OpenTelemetry instrumentation across heterogeneous codebases Introduction Years ago, I was tasked with buildin...

14 minAug 31