A pastebin takes a chunk of text, saves it, and gives you a short link to share — perfect for logs, config snippets, or error messages too long for chat. This one adds the feature that makes a pastebin actually usable: expiry, so pastes clean themselves up instead of piling up forever. It's three files on the Node.js application, and it reuses the same Express + SQLite shape as the URL shortener.
| At a glance | |
|---|---|
| You're building | A text-sharing service with per-paste expiry |
| You need | A Falix server running the Node.js application |
| Plan | Any — free runs while your session timer has time, premium runs 24/7 |
| Time | About thirty minutes |
If Node is new to you, read Node.js on Falix first.
What it does
| Feature | How |
|---|---|
| Create a paste | POST /api/paste with { content, expiry } returns a short id |
| View raw | GET /:id returns the paste as plain text |
| Expiry choices | 10 minutes, 1 hour, 1 day, 1 week, or never |
| Auto-cleanup | A timer deletes expired pastes every minute |
| Web form | public/index.html to write and save without curl |
The files
package.json:
{
"name": "pastebin",
"version": "1.0.0",
"private": true,
"main": "index.js",
"dependencies": {
"better-sqlite3": "^11.8.1",
"express": "^4.21.2"
}
}
index.js:
const express = require('express');
const Database = require('better-sqlite3');
const crypto = require('node:crypto');
const path = require('node:path');
const db = new Database('pastes.db');
db.exec(`CREATE TABLE IF NOT EXISTS pastes (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER
)`);
// How long each choice lasts, in seconds. null = never expires.
const TTL = { '10m': 600, '1h': 3600, '1d': 86400, '1w': 604800, never: null };
// Sweep expired pastes once a minute so the file doesn't grow forever.
setInterval(() => {
db.prepare('DELETE FROM pastes WHERE expires_at IS NOT NULL AND expires_at < ?')
.run(Date.now());
}, 60_000).unref();
const app = express();
app.use(express.json({ limit: '1mb' }));
app.use(express.static(path.join(__dirname, 'public')));
app.post('/api/paste', (req, res) => {
const { content, expiry } = req.body;
if (!content || typeof content !== 'string') {
return res.status(400).json({ error: 'content is required' });
}
const seconds = expiry in TTL ? TTL[expiry] : TTL['1d'];
const id = crypto.randomBytes(5).toString('base64url').slice(0, 8);
const expiresAt = seconds ? Date.now() + seconds * 1000 : null;
db.prepare('INSERT INTO pastes (id, content, created_at, expires_at) VALUES (?, ?, ?, ?)')
.run(id, content, Date.now(), expiresAt);
res.json({ id, url: `/${id}` });
});
// View a paste as raw text.
app.get('/:id', (req, res) => {
const row = db.prepare('SELECT content, expires_at FROM pastes WHERE id = ?')
.get(req.params.id);
if (!row || (row.expires_at && row.expires_at < Date.now())) {
return res.status(404).type('text/plain').send('Paste not found or expired');
}
res.type('text/plain').send(row.content);
});
const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));
public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pastebin</title>
</head>
<body>
<h1>New paste</h1>
<textarea id="content" rows="12" cols="60" placeholder="Paste your text here..."></textarea>
<div>
<label>Expires:
<select id="expiry">
<option value="10m">10 minutes</option>
<option value="1h">1 hour</option>
<option value="1d" selected>1 day</option>
<option value="1w">1 week</option>
<option value="never">Never</option>
</select>
</label>
<button id="save">Create paste</button>
</div>
<div id="out"></div>
<script>
document.getElementById('save').addEventListener('click', async () => {
const content = document.getElementById('content').value;
const expiry = document.getElementById('expiry').value;
const res = await fetch('/api/paste', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, expiry }),
});
const data = await res.json();
const out = document.getElementById('out');
if (data.url) {
const full = location.origin + data.url;
out.innerHTML = 'Saved: <a href="' + full + '">' + full + '</a>';
} else {
out.textContent = data.error || 'Something went wrong';
}
});
</script>
</body>
</html>
How it works
- Two timestamps decide a paste's life.
created_atrecords when it was made;expires_atis either a future millisecond time ornullfor "never". TheTTLtable maps each dropdown choice to a number of seconds — one place to change if you want different options. - Expiry is enforced twice, on purpose. The
GET /:idhandler treats an expired paste as a 404 immediately, so a paste is unreachable the second it lapses even if it's still on disk. The one-minutesetIntervalsweep is the janitor that actually frees the space. Belt and braces. .unref()keeps shutdown clean. It tells Node the cleanup timer shouldn't, by itself, keep the process alive — a small tidy-up detail.- Raw text on purpose.
res.type('text/plain')serves the paste as-is so code and logs show exactly as pasted, with nothing rendered or run.
🎯 Good to know: The
express.json({ limit: '1mb' })cap is your friend — without a limit, one giant paste could exhaust memory. On the free plan's 2.5 GB shared RAM, keep an eye on limits like this; see Out of memory.
Run it on Falix
- Upload the three files (make the
publicfolder forindex.html), or deploy from Git. - Press Start. The console runs
npm install, then printsListening on port …— your success signal and the line that flips the server online. - Open your address from the Network page, write something, pick an expiry, and save. Open the returned link to read it back.
The listen call already reads SERVER_PORT and binds 0.0.0.0, so there's nothing to configure for the port.
Command-line round-trip:
curl -X POST http://YOUR_ADDRESS:PORT/api/paste \
-H "Content-Type: application/json" \
-d '{"content": "hello\nworld", "expiry": "10m"}'
# -> {"id":"Gkfo1a2b","url":"/Gkfo1a2b"}
curl http://YOUR_ADDRESS:PORT/Gkfo1a2b
# -> hello
# world
🎯 Good to know: Pastes live in
pastes.dbon the server. Restarts keep them; a reinstall or application switch wipes the file. For storage that survives anything, move to a managed database.
Make it yours
- Syntax highlighting. Serve a view page that fetches the raw text and runs it through a client-side highlighter, keeping the raw route for
curl. - Burn after reading. Add a
viewslimit and delete the paste after the first read — handy for secrets. - A random title or line count shown on save, using the columns you already store.
- Rate limiting so nobody floods your database — pair with Protecting your API.
Beyond the Falix layer this is plain Express; expressjs.com covers middleware and routing in full.
Troubleshooting
content is required(400) — the request body was empty or not JSON. SendContent-Type: application/jsonand a non-emptycontentfield.- Paste 404s right after creating it — you chose a very short expiry, or the server restarted onto a fresh
pastes.db. Check the expiry you picked. Cannot find moduleon start — the install failed; read the npm output at the top of the console, or install from the Packages page and restart.- Page won't load at all — port/bind problem, not code: I can't reach my app.