Skip to content

Build Markdown Pastebin with live preview #1

@ibuzzardo

Description

@ibuzzardo

Overview

Build a web-based Markdown Pastebin where users can paste markdown text, get a unique shareable URL, and view rendered markdown with syntax highlighting. No authentication required — pastes are anonymous and permanent.

Requirements

  • Express.js backend serving both API and static frontend
  • TypeScript with strict mode
  • Create paste: POST /api/pastes accepts { content, title? } and returns { id, url, createdAt }
  • View paste: GET /api/pastes/:id returns { id, title, content, createdAt, viewCount }
  • Each paste gets a unique 8-character alphanumeric ID (nanoid)
  • View count increments on each GET request
  • Recent pastes: GET /api/pastes?limit=10 returns the 10 most recent pastes (title + id + createdAt only, no content)
  • Frontend served from /public as static HTML/CSS/JS
  • Home page (/) has a textarea for markdown input, a live preview panel, and a Create Paste button
  • After creating a paste, redirect to /:id which shows the rendered markdown
  • Paste view page (/:id) shows rendered HTML from markdown with syntax highlighting for code blocks
  • Raw view: GET /api/pastes/:id/raw returns plain text content with Content-Type: text/plain
  • GET /health returns { status: "ok", pasteCount: N }
  • In-memory storage using a Map (no database)
  • Maximum paste size: 100KB — reject larger with 413
  • Global error handling middleware

File Structure

src/
├── index.ts              # Entry point — Express server, static files, routes
├── routes/
│   ├── pastes.ts         # CRUD API handlers for pastes
│   └── health.ts         # Health check endpoint
├── middleware/
│   ├── errorHandler.ts   # Global error handling middleware
│   └── sizeLimit.ts      # Request body size validation (100KB max)
├── lib/
│   ├── store.ts          # In-memory paste storage (Map-based)
│   └── id.ts             # ID generation (nanoid, 8 chars)
├── types.ts              # TypeScript interfaces (Paste, CreatePasteRequest)
public/
├── index.html            # Home page — create paste with live preview
├── view.html             # Paste view page — rendered markdown
├── css/
│   └── style.css         # Styling — clean, minimal, responsive
├── js/
│   ├── editor.js         # Live preview logic (textarea to rendered markdown)
│   ├── view.js           # Fetch and render paste on view page
│   └── lib/
│       └── marked.min.js # Markdown parser (bundled, no CDN)
tests/
├── pastes.test.ts        # Integration tests for paste CRUD
└── store.test.ts         # Unit tests for in-memory store
package.json
tsconfig.json
README.md

Dependencies

  • express@4 — HTTP server and static file serving
  • nanoid@5 — Unique ID generation
  • typescript@5 — Type safety
  • tsx@4 — TypeScript execution
  • vitest@1 — Test runner
  • supertest@6 — HTTP testing

Acceptance Criteria

  • npm install && npm start runs the server on PORT (default 3000)
  • GET / serves the home page with a textarea and live preview
  • Typing markdown in the textarea shows rendered HTML in the preview panel in real-time
  • POST /api/pastes with { content: "# Hello" } returns 201 with { id, url, createdAt }
  • GET /api/pastes/:id returns the paste with viewCount incremented
  • GET /:id serves the view page which fetches and renders the paste markdown as HTML
  • Code blocks in the rendered markdown have syntax highlighting
  • GET /api/pastes/:id/raw returns plain text with correct Content-Type
  • GET /api/pastes?limit=5 returns the 5 most recent pastes
  • POST with body larger than 100KB returns 413
  • POST with empty content returns 400
  • GET /api/pastes/nonexistent returns 404
  • GET /health returns { status: "ok", pasteCount: N }
  • TypeScript compiles with strict: true
  • npm test passes all tests
  • The UI is clean and responsive (works on mobile width)

Edge Cases

  • Empty content field returns 400 with "content is required"
  • Content over 100KB returns 413 with "paste too large"
  • Non-existent paste ID returns 404 with "paste not found"
  • Missing title defaults to "Untitled"
  • Special characters in markdown (HTML injection) must be sanitized in rendered output
  • Very long title truncated to 200 chars
  • Concurrent view count increments are acceptable to lose (in-memory, no locks needed)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions