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:fsto 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=linuxor/api/posts?search=crm)
API routes
| Route | Description |
|---|---|
GET /api/posts | List all posts (metadata only, no body) |
GET /api/posts?tag=architecture | Filter by tag |
GET /api/posts?search=gnomad | Search title + summary |
GET /api/posts/:slug | Full 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_modulesattack surface beyond dev tooling - Predictable memory usage with a simple
Mapcache - Hot reload driven by the filesystem, not a build step
- JSON endpoints any frontend (including Astro) can consume
Related work
- Project 2: Local-LLM Log Anomaly CLI — same zero-dependency philosophy in Python + Ollama
- Project 3: Legacy Flat-File Bridge — enterprise fixed-width parsing in modern Java
- Project 4: Recursive Hierarchy Builder — PostgreSQL recursive CTE to nested JSON
- Project 5: Concurrent Link Sweeper — Go goroutines for network sweeps
- Project 6: Local Context Server — MCP-style Linux tool gateway for agents
- Project 7: Multi-Tenant Migration Engine — 2PC fleet migrations for Gnomad CRM
- Project 8: Async TCP Gateway — raw socket layer beneath HTTP abstractions
- Gnomad CRM architecture write-up and agentic workflow notes — local-first philosophy at a different layer of the stack