Initial template
This commit is contained in:
commit
ec6beec87b
4 changed files with 136 additions and 0 deletions
12
Dockerfile
Normal file
12
Dockerfile
Normal 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
9
README.md
Normal 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
12
package.json
Normal 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
103
server.js
Normal 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}`));
|
||||
});
|
||||
Loading…
Reference in a new issue