Build a Code Snippet Sharer (Pastebin Clone)
In this tutorial, you'll learn about Build a Code Snippet Sharer (Pastebin Clone). We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Build a Pastebin-like code snippet sharing tool with Node.js that supports syntax-highlighted views, automatic expiration, raw text output, and shareable short links.
What You'll Build
You'll build a web application where users can paste code, select a programming language for syntax highlighting, optionally set an expiration time, and receive a short URL to share. The raw text endpoint lets other tools fetch the snippet programmatically — useful for paste-from-terminal workflows.
Why a Code Sharer Matters
Developers share code constantly — in bug reports, code reviews, tutorials, and chats. A dedicated snippet tool beats screenshots or email attachments because it preserves formatting, enables syntax highlighting, and supports direct raw downloads. The same storage-and-retrieval pattern is used in security tools like Durga Antivirus Pro's quarantine system, where suspicious code snippets are stored with metadata for later analysis.
Prerequisites
- Node.js 18+ installed
- Basic Express.js routing knowledge
- Familiarity with SQLite for data persistence
Step 1: Project Setup
mkdir snippet-sharer
cd snippet-sharer
npm init -y
npm install express better-sqlite3 highlight.js nanoid
highlight.js provides syntax highlighting. nanoid generates short unique IDs for snippet URLs — smaller than UUIDs and URL-safe.
Step 2: Database and Storage
// database.js
const Database = require('better-sqlite3');
const db = new Database('snippets.db');
db.exec(`
CREATE TABLE IF NOT EXISTS snippets (
id TEXT PRIMARY KEY,
title TEXT DEFAULT 'Untitled',
content TEXT NOT NULL,
language TEXT DEFAULT 'plaintext',
created_at TEXT DEFAULT (datetime('now')),
expires_at TEXT NULL
);
`);
// Auto-delete expired snippets
setInterval(() => {
db.prepare("DELETE FROM snippets WHERE expires_at IS NOT NULL AND expires_at < datetime('now')").run();
}, 60000); // Clean every 60 seconds
module.exports = db;
The expires_at column lets us auto-delete snippets after a set time. A background interval runs every 60 seconds to purge expired entries — keeping the database small and fast.
Step 3: Express Routes
// server.js
const express = require('express');
const { nanoid } = require('nanoid');
const hljs = require('highlight.js');
const db = require('./database');
const app = express();
app.use(express.json());
app.use(express.static('public'));
// Supported languages mapped to highlight.js keys
const LANGUAGES = {
javascript: 'js', python: 'python', html: 'xml', css: 'css',
json: 'json', bash: 'bash', sql: 'sql', yaml: 'yaml',
markdown: 'markdown', plaintext: 'plaintext'
};
app.post('/api/snippets', (req, res) => {
const { content, language, title, expiration_minutes } = req.body;
if (!content || !content.trim()) {
return res.status(400).json({ error: 'Content is required' });
}
const id = nanoid(10);
const lang = LANGUAGES[language] || 'plaintext';
let expires_at = null;
if (expiration_minutes && Number(expiration_minutes) > 0) {
const expiry = new Date();
expiry.setMinutes(expiry.getMinutes() + Number(expiration_minutes));
expires_at = expiry.toISOString().slice(0, 19).replace('T', ' ');
}
db.prepare(
'INSERT INTO snippets (id, title, content, language, expires_at) VALUES (?, ?, ?, ?, ?)'
).run(id, title || 'Untitled', content, lang, expires_at);
res.json({ id, url: `/snippet/${id}`, raw_url: `/raw/${id}` });
});
app.get('/api/snippets/:id', (req, res) => {
const snippet = db.prepare('SELECT * FROM snippets WHERE id = ?').get(req.params.id);
if (!snippet) return res.status(404).json({ error: 'Snippet not found or expired' });
res.json(snippet);
});
app.get('/raw/:id', (req, res) => {
const snippet = db.prepare('SELECT * FROM snippets WHERE id = ?').get(req.params.id);
if (!snippet) return res.status(404).send('Snippet not found');
res.set('Content-Type', 'text/plain');
res.send(snippet.content);
});
app.get('/snippet/:id', (req, res) => {
const snippet = db.prepare('SELECT * FROM snippets WHERE id = ?').get(req.params.id);
if (!snippet) return res.status(404).sendFile(__dirname + '/public/404.html');
const highlighted = hljs.highlight(snippet.content, {
language: snippet.language === 'plaintext' ? 'plaintext' : snippet.language
}).value;
const html = `<!DOCTYPE html>
<html><head><title>${snippet.title} - Snippet Sharer</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui; background: #0d1117; color: #c9d1d9; }
.header { display: flex; justify-content: space-between; align-items: center;
padding: 12px 20px; background: #161b22; border-bottom: 1px solid #30363d; }
.header h1 { font-size: 16px; font-weight: 600; }
.header a { color: #58a6ff; text-decoration: none; font-size: 14px; }
pre { padding: 20px; overflow-x: auto; margin: 0; }
.meta { padding: 8px 20px; font-size: 12px; color: #8b949e; background: #161b22; border-bottom: 1px solid #30363d; }
</style></head><body>
<div class="header">
<h1>${snippet.title}</h1>
<div>
<a href="/raw/${snippet.id}">Raw</a>
</div>
</div>
<div class="meta">
Language: ${snippet.language} | Created: ${snippet.created_at}
${snippet.expires_at ? '| Expires: ' + snippet.expires_at : ''}
</div>
<pre><code class="hljs language-${snippet.language}">${highlighted}</code></pre>
</body></html>`;
res.send(html);
});
app.listen(4000, () => console.log('Snippet sharer on port 4000'));
The server supports JSON API for programmatic access and HTML rendering for browser viewing. The /raw/:id endpoint returns pure text — useful for curl or piping into other tools.
Expected output: POST a JSON body with { "content": "console.log('hello')", "language": "<a href="/programming-languages/javascript/">JavaScript</a>" } to /api/snippets. You'll receive { "id": "abc123xyz", "url": "/snippet/abc123xyz", "raw_url": "/raw/abc123xyz" }. Visit the URL to see syntax-highlighted code.
Step 4: Frontend Paste Form
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head><title>Code Snippet Sharer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 20px; background: #0d1117; color: #c9d1d9; }
textarea, select, input { width: 100%; padding: 12px; border: 1px solid #30363d; border-radius: 6px;
background: #161b22; color: #c9d1d9; font-size: 14px; margin-bottom: 12px; }
textarea { min-height: 300px; font-family: 'Courier New', monospace; }
button { padding: 12px 24px; background: #238636; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; }
button:hover { background: #2ea043; }
#result { margin-top: 16px; padding: 16px; background: #161b22; border-radius: 6px; display: none; }
#result a { color: #58a6ff; }
</style></head>
<body>
<h1>Share Code Snippet</h1>
<p style="color: #8b949e; margin-bottom: 16px">Paste code below and get a shareable link.</p>
<input id="titleInput" placeholder="Title (optional)">
<select id="langSelect">
<option value="plaintext">Plain Text</option>
<option value="javascript">JavaScript</option>
<option value="python">Python</option>
<option value="html">HTML</option>
<option value="css">CSS</option>
<option value="json">JSON</option>
<option value="bash">Bash</option>
<option value="sql">SQL</option>
<option value="yaml">YAML</option>
</select>
<textarea id="codeInput" placeholder="Paste your code here..."></textarea>
<div style="display: flex; gap: 12px; align-items: center">
<select id="expirySelect" style="width: auto">
<option value="">Never expire</option>
<option value="10">10 minutes</option>
<option value="60">1 hour</option>
<option value="1440">24 hours</option>
<option value="10080">7 days</option>
</select>
<button onclick="createSnippet()">Create Snippet</button>
</div>
<div id="result">
<p>Share this link:</p>
<a id="snippetLink" href="#" target="_blank"></a>
<p style="margin-top: 8px">Raw view:</p>
<a id="rawLink" href="#" target="_blank"></a>
</div>
<script>
async function createSnippet() {
const content = document.getElementById('codeInput').value;
if (!content.trim()) return alert('Paste some code first');
const res = await fetch('/api/snippets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content,
language: document.getElementById('langSelect').value,
title: document.getElementById('titleInput').value || 'Untitled',
expiration_minutes: document.getElementById('expirySelect').value
})
});
const data = await res.json();
if (data.error) return alert(data.error);
document.getElementById('snippetLink').textContent = window.location.origin + data.url;
document.getElementById('snippetLink').href = data.url;
document.getElementById('rawLink').textContent = window.location.origin + data.raw_url;
document.getElementById('rawLink').href = data.raw_url;
document.getElementById('result').style.display = 'block';
}
</script>
</body>
</html>
Expected output: Open http://localhost:4000 in a browser. Paste code, select a language, optionally set expiry, click "Create Snippet". The page shows a shareable link. Open the link in a new tab — code displays with GitHub-dark syntax highlighting.
Architecture
flowchart LR
A[Browser] -->|POST /api/snippets| B[Express Server]
B --> C[SQLite Database]
B --> D[highlight.js]
D --> E[Syntax Highlighted HTML]
A -->|GET /snippet/:id| B
B --> C
B --> E
A -->|GET /raw/:id| B
B --> C
B -->|text/plain| A
Common Errors
1. Syntax highlighting doesn't match the language
The language key sent to the API must match one of the supported languages. If you send "language": "js" instead of "<a href="/programming-languages/javascript/">JavaScript</a>", highlight.js falls back to plaintext. Use our LANGUAGES map to translate user-friendly names to highlight.js keys.
2. Expired snippets still accessible
The background cleanup runs every 60 seconds, so a snippet could be accessible up to 59 seconds past its expiration. Check expires_at in the GET handler too: if (snippet.expires_at && new Date(snippet.expires_at) < new Date()) return 404.
3. XSS via raw snippet content
The /raw/:id endpoint sets Content-Type: text/plain, which prevents the browser from executing HTML in snippet content. Never use text/html for raw output — this is a common security pitfall.
Practice Questions
1. Why use nanoid instead of sequential IDs? Sequential IDs like 1, 2, 3 let anyone enumerate all snippets. Nanoid generates 10-character random strings — practically unguessable. This also prevents ID conflicts in Distributed Systems.
2. How does the raw text view differ from the HTML view?
Raw view returns Content-Type: text/plain — just the code, no styling. It's designed for curl, wget, or embedding in other tools. The HTML view wraps the code in a styled page with syntax highlighting and metadata.
3. What happens when a snippet expires? The background cleanup job deletes it from SQLite. Future requests for that ID return 404. The TTL-based approach keeps the database size bounded and prevents stale content.
4. Challenge: Clipboard integration
Add a "Copy Link" button that copies the snippet URL to the clipboard using navigator.clipboard.writeText(). Show a brief "Copied!" confirmation message.
FAQ
Next Steps
- Add user accounts so people can view and manage their snippets
- Explore input validation techniques for user-submitted content
- Try building the Link Preview Service for another content-processing API
- Learn about Docker deployment to deploy your snippet sharer to the cloud
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro