Initial template

This commit is contained in:
ezkeel-admin 2026-06-11 18:49:06 +00:00
commit ec6beec87b
4 changed files with 136 additions and 0 deletions

12
Dockerfile Normal file
View file

@ -0,0 +1,12 @@
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY server.js ./
EXPOSE 3000
CMD ["npm", "start"]

9
README.md Normal file
View file

@ -0,0 +1,9 @@
# url-shortener
Express + PostgreSQL link shortener.
- `POST /api/links` with `{"url": "https://example.com"}` returns a 7-char hash and short URL
- `GET /<hash>` 302-redirects to the original URL and counts the hit
- `GET /api/links` lists recent links with hit counts
Reads `DATABASE_URL` and `PORT` from the environment — EZKeel provides both at deploy time.

12
package.json Normal file
View file

@ -0,0 +1,12 @@
{
"name": "url-shortener",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.3"
}
}

103
server.js Normal file
View file

@ -0,0 +1,103 @@
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}`));
});