url-shortener/server.js
2026-06-11 18:49:06 +00:00

103 lines
2.7 KiB
JavaScript

const express = require('express');
const crypto = require('crypto');
const { Pool } = require('pg');
const app = express();
app.use(express.json());
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
async function initDB() {
const client = await pool.connect();
try {
await client.query(`
CREATE TABLE IF NOT EXISTS links (
hash TEXT PRIMARY KEY,
url TEXT NOT NULL,
hits INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
)
`);
console.log('Database table ready');
} finally {
client.release();
}
}
function makeHash(url) {
return crypto.createHash('sha256').update(url).digest('base64url').slice(0, 7);
}
// POST /api/links {"url": "https://example.com"} -> {"hash": "...", "short_url": "..."}
app.post('/api/links', async (req, res) => {
const { url } = req.body;
let parsed;
try {
parsed = new URL(url);
} catch {
return res.status(400).json({ error: '"url" must be a valid URL' });
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return res.status(400).json({ error: 'only http/https URLs allowed' });
}
const hash = makeHash(parsed.href);
try {
await pool.query(
'INSERT INTO links (hash, url) VALUES ($1, $2) ON CONFLICT (hash) DO NOTHING',
[hash, parsed.href]
);
res.status(201).json({
hash,
short_url: `${req.protocol}://${req.get('host')}/${hash}`,
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to create link' });
}
});
// GET /api/links -> recent links with hit counts
app.get('/api/links', async (req, res) => {
try {
const { rows } = await pool.query(
'SELECT hash, url, hits, created_at FROM links ORDER BY created_at DESC LIMIT 50'
);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to list links' });
}
});
// GET /:hash -> 302 redirect
app.get('/:hash([A-Za-z0-9_-]{7})', async (req, res) => {
try {
const { rows } = await pool.query(
'UPDATE links SET hits = hits + 1 WHERE hash = $1 RETURNING url',
[req.params.hash]
);
if (rows.length === 0) return res.status(404).json({ error: 'Link not found' });
res.redirect(302, rows[0].url);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to resolve link' });
}
});
app.get('/', (req, res) => {
res.json({
name: 'url-shortener',
usage: {
shorten: 'POST /api/links {"url": "https://example.com"}',
list: 'GET /api/links',
follow: 'GET /<hash>',
},
});
});
initDB().then(() => {
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
});