Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion nginx/default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ server {
root /var/www/html/muhzi.com;
}
location ~* "^/[0-9a-z@]{5,15}$" {
rewrite ^/(.*)$ http://muhzi.com:7000/api/item/$1 redirect;
proxy_pass http://127.0.0.1:7000;
rewrite ^/(.*)$ /api/item/$1 break;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

48 changes: 27 additions & 21 deletions nginx/shortener_default.conf
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
server {
listen 80;
listen [::]:80;
index index.html index.htm index.nginx-debian.html;
server_name shortener.muhzi.com;
root /var/www/html/shortener;
location / {
root /var/www/html/shortener;
}
location /v1/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://127.0.0.1:7000/;
proxy_redirect off;
}

}
u
server {
listen 80;
listen [::]:80;
index index.html index.htm index.nginx-debian.html;
server_name shortener.muhzi.com;
root /var/www/html/shortener;
location / {
root /var/www/html/shortener;
}
location /v1/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://127.0.0.1:7000/;
proxy_redirect off;
}
location ~* "^/[0-9a-z@]{5,15}$" {
proxy_pass http://127.0.0.1:7000;
rewrite ^/(.*)$ /api/item/$1 break;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

}
2 changes: 1 addition & 1 deletion server/config/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = {
mongoURI: "mongodb://localhost/url-shortner",
errorUrl: "https://muhzi.com/error"
errorUrl: "http://localhost/error"
};
99 changes: 69 additions & 30 deletions server/routes/urlshorten.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,94 @@ const constants = require('../config/constants');
const shortCode = require('../middlewares/uniqueUrlCode');

const cache = require('../services/cache');

module.exports = app => {
// GET /api/item/:code — redirect to the original URL
app.get('/api/item/:code', async (req, res) => {
const urlCode = req.params.code;
const item = await UrlShorten.findOne({ urlCode: urlCode });
if (item) {
return res.redirect(item.originalUrl);
} else {

try {
// 1. Check Redis cache first (avoids a DB round-trip on every redirect)
let urlData = await cache.getFromCache('urlCode', urlCode);

if (!urlData) {
// 2. Fall back to MongoDB
urlData = await UrlShorten.findOne({ urlCode }).exec();

// 3. Populate cache for next time
if (urlData) {
cache.addToCache('urlCode', urlCode, urlData);
}
}

if (urlData) {
return res.redirect(urlData.originalUrl);
} else {
return res.redirect(constants.errorUrl);
}
} catch (err) {
// Cache or DB error — still try a plain DB lookup so the redirect works
try {
const urlData = await UrlShorten.findOne({ urlCode }).exec();
if (urlData) return res.redirect(urlData.originalUrl);
} catch (_) { /* intentional */ }
return res.redirect(constants.errorUrl);
}
});

// POST /api/item — shorten a URL
app.post('/api/item', async (req, res) => {
const { shortBaseUrl, originalUrl } = req.body;
if (validUrl.isUri(shortBaseUrl)) {
} else {

if (!validUrl.isUri(shortBaseUrl)) {
return res.status(404).json('Invalid Base Url format');
}

if (!validUrl.isUri(originalUrl)) {
return res.status(401).json('Invalid Original Url.');
}

const updatedAt = new Date();
const queryOptions = { originalUrl };
if (validUrl.isUri(originalUrl)) {

try {
let urlData;

// Check Redis cache
try {
// Find the item is in the cache
urlData = await cache.getFromCache('originalUrl', JSON.stringify(queryOptions));
if (!urlData) {
// Find the item is in the database
urlData = await UrlShorten.findOne(queryOptions).exec();
}
} catch (_) {
// Redis unavailable — skip cache, fall through to DB
urlData = null;
}

if (urlData) {
res.status(200).json(urlData);
} else {
const urlCode = shortCode.generate();
shortUrl = shortBaseUrl + '/' + urlCode;
const itemToBeSaved = { originalUrl, shortUrl, urlCode, updatedAt };

// Add the item to db
const item = new UrlShorten(itemToBeSaved);
await item.save();
// Add the item to cache
cache.addToCache('originalUrl', JSON.stringify(queryOptions), itemToBeSaved);
res.status(200).json(itemToBeSaved);
}
} catch (err) {
res.status(401).json('Invalid User Id');
if (!urlData) {
// Check MongoDB
urlData = await UrlShorten.findOne(queryOptions).exec();
}
} else {
return res.status(401).json('Invalid Original Url.');

if (urlData) {
return res.status(200).json(urlData);
}

// Brand-new URL — generate a short code and persist
const urlCode = shortCode.generate();
const shortUrl = shortBaseUrl + '/' + urlCode; // fixed: was undeclared
const itemToBeSaved = { originalUrl, shortUrl, urlCode, updatedAt };

const item = new UrlShorten(itemToBeSaved);
await item.save();

// Add to cache (best-effort; ignore Redis errors)
try {
cache.addToCache('originalUrl', JSON.stringify(queryOptions), itemToBeSaved);
cache.addToCache('urlCode', urlCode, itemToBeSaved);
} catch (_) { /* intentional */ }

return res.status(200).json(itemToBeSaved);
} catch (err) {
console.error('POST /api/item error:', err);
return res.status(500).json('Internal server error');
}
});
};
4 changes: 3 additions & 1 deletion server/services/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ const keys = require('../config/redis');
const client = redis.createClient(keys.redisUrl);
client.hget = util.promisify(client.hget);

client.on('error', function(err) {});
client.on('error', function(err) {
console.error('[Redis] Connection error:', err.message);
});
// eslint-disable-next-line
const exec = mongoose.Query.prototype.exec;

Expand Down