Case Study

Zero-Dependency Markdown API Router

Production-ready TypeScript API using native Node.js modules — in-memory caching, file watching, and query routing without Express or YAML parsers.

  • Node.js
  • TypeScript
  • node:http
  • node:fs
  • tsx
  • typescript
  • node
  • api
  • zero-dependency
  • markdown

Overview

A complete, production-ready implementation using native Node.js modules (node:http, node:fs, node:path). It operates entirely without external dependencies like Express, Fastify, or a YAML front-matter parser.

What it implements

  • In-memory caching — parses and stores post metadata dynamically
  • Native file watching — uses node:fs to watch your Markdown directory and instantly re-parse files when they are modified, added, or deleted
  • Advanced native query routing — supports URL filtering via native URLSearchParams (e.g. /api/posts?tag=linux or /api/posts?search=crm)

API routes

RouteDescription
GET /api/postsList all posts (metadata only, no body)
GET /api/posts?tag=architectureFilter by tag
GET /api/posts?search=gnomadSearch title + summary
GET /api/posts/:slugFull post including markdown body

Project setup

Ensure you have Node.js installed. Create a clean folder, initialize the project, and configure TypeScript:

mkdir md-api-router && cd md-api-router
npm init -y
npm install -D typescript @types/node tsx
npx tsc --init

Create a directory named content and drop sample .md or .mdx files inside with front-matter like this:

---
title: Building Gnomad CRM Architecture
date: 2026-02-15
tags: typescript, architecture, crm
summary: A deep dive into multi-tenant database isolation strategies.
---
Your actual post markdown content goes here...

The code (server.ts)

Create server.ts with the following implementation:

import http from 'node:http';
import fs from 'node:fs/promises';
import { watch } from 'node:fs';
import path from 'node:path';
import { URL } from 'node:url';

// --- Interfaces & Types ---
interface PostMetadata {
  title: string;
  date: string;
  tags: string[];
  summary: string;
  slug: string;
  [key: string]: any;
}

interface Post extends PostMetadata {
  content: string;
}

// --- Configuration & Cache Memory ---
const CONTENT_DIR = path.join(process.cwd(), 'content');
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 4000;
const postCache = new Map<string, Post>();

// --- Helper: Native Front-Matter & Markdown Parser ---
function parseMarkdown(fileContent: string, slug: string): Post {
  const frontMatterRegex = /^---([\s\S]*?)---/;
  const match = fileContent.match(frontMatterRegex);

  const metadata: Partial<PostMetadata> = { slug, tags: [] };
  let content = fileContent;

  if (match) {
    content = fileContent.replace(frontMatterRegex, '').trim();
    const rawLines = match[1].split('\n');

    for (const line of rawLines) {
      const splitIdx = line.indexOf(':');
      if (splitIdx === -1) continue;

      const key = line.slice(0, splitIdx).trim();
      const value = line.slice(splitIdx + 1).trim();

      if (key === 'tags') {
        metadata.tags = value.split(',').map((t) => t.trim().toLowerCase());
      } else {
        metadata[key] = value;
      }
    }
  }

  return {
    title: metadata.title || 'Untitled Post',
    date: metadata.date || new Date().toISOString().split('T')[0],
    tags: metadata.tags || [],
    summary: metadata.summary || '',
    slug,
    content,
  };
}

// --- Cache Management Operations ---
async function loadAllPosts() {
  try {
    const entries = await fs.readdir(CONTENT_DIR, { withFileTypes: true });
    postCache.clear();

    for (const entry of entries) {
      if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.mdx'))) {
        const filePath = path.join(CONTENT_DIR, entry.name);
        const fileContent = await fs.readFile(filePath, 'utf-8');
        const slug = path.parse(entry.name).name;

        const parsedPost = parseMarkdown(fileContent, slug);
        postCache.set(slug, parsedPost);
      }
    }
    console.log(`[Cache] Successfully indexed ${postCache.size} posts.`);
  } catch (err) {
    console.error('[Cache Error] Failed reading content directory:', err);
  }
}

function startFileWatcher() {
  watch(CONTENT_DIR, async (eventType, filename) => {
    if (!filename) return;
    if (filename.endsWith('.md') || filename.endsWith('.mdx')) {
      console.log(`[Watcher] File modification detected (${eventType}): ${filename}`);
      await loadAllPosts();
    }
  });
}

// --- Native HTTP Server Router ---
const server = http.createServer(async (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');

  if (req.method === 'OPTIONS') {
    res.writeHead(204);
    res.end();
    return;
  }

  const parsedUrl = new URL(req.url || '', `http://${req.headers.host}`);
  const pathname = parsedUrl.pathname;

  // ROUTE 1: GET /api/posts (Supports optional ?tag= and ?search= queries)
  if (pathname === '/api/posts' && req.method === 'GET') {
    const searchParam = parsedUrl.searchParams.get('search')?.toLowerCase();
    const tagParam = parsedUrl.searchParams.get('tag')?.toLowerCase();

    let postsArray = Array.from(postCache.values()).map(({ content, ...metadata }) => metadata);

    if (tagParam) {
      postsArray = postsArray.filter((post) => post.tags.includes(tagParam));
    }

    if (searchParam) {
      postsArray = postsArray.filter(
        (post) =>
          post.title.toLowerCase().includes(searchParam) ||
          post.summary.toLowerCase().includes(searchParam),
      );
    }

    res.writeHead(200);
    res.end(JSON.stringify(postsArray, null, 2));
    return;
  }

  // ROUTE 2: GET /api/posts/:slug (Returns individual post with full body)
  if (pathname.startsWith('/api/posts/') && req.method === 'GET') {
    const slug = pathname.replace('/api/posts/', '');
    const post = postCache.get(slug);

    if (!post) {
      res.writeHead(404);
      res.end(JSON.stringify({ error: `Post with slug '${slug}' not found.` }));
      return;
    }

    res.writeHead(200);
    res.end(JSON.stringify(post, null, 2));
    return;
  }

  // FALLBACK: 404 Route Not Found
  res.writeHead(404);
  res.end(JSON.stringify({ error: 'Route not found. Use /api/posts or /api/posts/:slug' }));
});

// --- Boot Routine ---
async function main() {
  try {
    await fs.mkdir(CONTENT_DIR, { recursive: true });
  } catch (err) {}

  await loadAllPosts();
  startFileWatcher();

  server.listen(PORT, () => {
    console.log(`\x1b[36m%s\x1b[0m`, `🚀 Native Markdown API Hub running at http://localhost:${PORT}`);
    console.log(`👉 Available Routes:`);
    console.log(`   - http://localhost:${PORT}/api/posts`);
    console.log(`   - http://localhost:${PORT}/api/posts?tag=architecture`);
    console.log(`   - http://localhost:${PORT}/api/posts?search=gnomad`);
    console.log(`   - http://localhost:${PORT}/api/posts/<slug-name>\n`);
  });
}

main();

Execution

Run directly with tsx (TypeScript execute):

npx tsx server.ts

When you modify a markdown file or add a new one in /content, the watcher logs fire immediately and the API cache updates without a server restart.


Why zero dependencies?

This pattern is ideal for homelab deployments, agent sandboxes, and sidecar APIs where you want:

  • No node_modules attack surface beyond dev tooling
  • Predictable memory usage with a simple Map cache
  • Hot reload driven by the filesystem, not a build step
  • JSON endpoints any frontend (including Astro) can consume