DEV Community

Miklós Bodor
Miklós Bodor

Posted on

I needed Markdown JSON in four pipelines, so I shipped one endpoint that does it once

The same parser, four times

Over the last year I kept running into the same shape of problem:

  1. A docs site generator that wanted Markdown chapters turned into navigation JSON.
  2. A RAG ingestion script where each Markdown file needed to become a list of text chunks plus its frontmatter metadata.
  3. An n8n flow that took Markdown emails and extracted only the tasklists.
  4. A static-site backend that accepted user Markdown and needed to validate structure before persisting.

Each one is small on its own. But every time I reached for a different library — remark here, gray-matter there, marked once, a hand-rolled regex once too many — and every time one of them broke on the same edge cases:

  • Nested GFM tasklists where the checked state was silently lost
  • YAML frontmatter that included quoted booleans (parsed as strings, not booleans)
  • Tables whose headers contained spaces (regex parsers treated them as one key)
  • Code blocks containing Markdown — re-parsed as Markdown instead of fenced code

So I built one endpoint that does it once, properly.

What it returns

POST /v1/parse takes a Markdown body (text/markdown) or a JSON envelope
(application/json) and returns one stable JSON shape:

{
  "success": true,
  "data": {
    "title": "Project Alpha",
    "frontmatter": { "title": "Project Alpha", "status": "shipping" },
    "headings":  [ { "level": 1, "text": "Project Alpha", "id": "project-alpha" } ],
    "sections":  [ { "heading": { ... }, "children": [ ... ], "content": [ ... ] } ],
    "lists":     [ { "ordered": false, "items": ["ship MVP", "write README"] } ],
    "tasklists": [ { "items": [ { "text": "ship MVP", "checked": true } ] } ],
    "tables":    [ { "headers": ["Module","Status"], "rows": [{"Module":"API","Status":"Done"}] } ],
    "codeBlocks":[ { "lang":