├── .gitignore ├── QRCodes └── QllW2Jsoz.png ├── errors ├── custom-error.js ├── not-found.js └── bad-request.js ├── routes ├── url.js └── main.js ├── db └── connect.js ├── views ├── error.ejs └── home.ejs ├── controllers ├── visitUrl.js └── url.js ├── models └── Url.js ├── package.json ├── middlewares └── errorHandler.js ├── README.md ├── app.js └── public ├── main.js └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | /node_modules -------------------------------------------------------------------------------- /QRCodes/QllW2Jsoz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedGsa/MyURLShortener/HEAD/QRCodes/QllW2Jsoz.png -------------------------------------------------------------------------------- /errors/custom-error.js: -------------------------------------------------------------------------------- 1 | class CustomAPIError extends Error { 2 | constructor(message, statusCode) { 3 | super(message); 4 | this.message = message; 5 | this.statusCode = statusCode; 6 | } 7 | } 8 | 9 | module.exports = CustomAPIError -------------------------------------------------------------------------------- /routes/url.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const {createShortenUrl, getQRImage} = require("../controllers/url") 4 | 5 | router.route("/").post(createShortenUrl) 6 | router.route("/image/:shortId").get(getQRImage) 7 | 8 | module.exports = router -------------------------------------------------------------------------------- /db/connect.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose");7 2 | mongoose.set("strictQuery", false); 3 | 4 | const connectDB = async (url) => { 5 | try { 6 | await mongoose.connect(url); 7 | } catch (error) { 8 | console.log(error); 9 | } 10 | } 11 | 12 | module.exports = connectDB; -------------------------------------------------------------------------------- /routes/main.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const visitUrl = require("../controllers/visitUrl") 4 | 5 | router.route("/").get((req,res) => { 6 | res.render("home.ejs"); 7 | }); 8 | router.route("/:shortId").get(visitUrl); 9 | 10 | module.exports = router -------------------------------------------------------------------------------- /errors/not-found.js: -------------------------------------------------------------------------------- 1 | const CustomAPIError = require("./custom-error"); 2 | const {StatusCodes} = require("http-status-codes") 3 | 4 | class NotFoundError extends CustomAPIError { 5 | constructor(message) { 6 | super(message, StatusCodes.NOT_FOUND) 7 | } 8 | } 9 | 10 | module.exports = NotFoundError -------------------------------------------------------------------------------- /errors/bad-request.js: -------------------------------------------------------------------------------- 1 | const CustomAPIError = require("./custom-error"); 2 | const {StatusCodes} = require("http-status-codes") 3 | 4 | class BadRequestError extends CustomAPIError { 5 | constructor(message) { 6 | super(message, StatusCodes.BAD_REQUEST) 7 | } 8 | } 9 | 10 | module.exports = BadRequestError -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Error 8 | 9 | 10 | <% if(msg && status) { %> 11 |

Error <%=status%>

12 |

<%=msg%>

13 | <% } %> 14 | 15 | -------------------------------------------------------------------------------- /controllers/visitUrl.js: -------------------------------------------------------------------------------- 1 | const BadRequestError = require("../errors/bad-request"); 2 | const NotFoundError = require("../errors/not-found"); 3 | const Url = require("../models/Url"); 4 | 5 | 6 | const visitUrl = async (req,res) => { 7 | const {shortId} = req.params; 8 | const url = await Url.findOne({shortId}); 9 | if(!url) { 10 | throw new NotFoundError("Url doesn't Exist") 11 | } 12 | res.redirect(url.longUrl); 13 | } 14 | 15 | module.exports = visitUrl -------------------------------------------------------------------------------- /models/Url.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose") 2 | 3 | const urlSchema = mongoose.Schema({ 4 | shortUrl: { 5 | type: String, 6 | required: [true, "Please provide short Url"] 7 | }, 8 | longUrl: { 9 | type: String, 10 | required: [true, "Please provide long Url"] 11 | }, 12 | shortId: { 13 | type: String, 14 | required: [true, "Please provide the short id"] 15 | } 16 | }); 17 | 18 | module.exports = mongoose.model("Url", urlSchema); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myurlshortener", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "nodemon app.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^1.3.1", 14 | "cors": "^2.8.5", 15 | "dotenv": "^16.0.3", 16 | "ejs": "^3.1.8", 17 | "express": "^4.18.2", 18 | "express-async-errors": "^3.1.1", 19 | "http-status-codes": "^2.2.0", 20 | "mongoose": "^6.9.0", 21 | "qrcode": "^1.5.1", 22 | "shortid": "^2.2.16", 23 | "valid-url": "^1.0.9" 24 | }, 25 | "devDependencies": { 26 | "nodemon": "^2.0.20" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /middlewares/errorHandler.js: -------------------------------------------------------------------------------- 1 | const CustomAPIError = require("../errors/custom-error"); 2 | const {StatusCodes} = require("http-status-codes"); 3 | const BadRequestError = require("../errors/bad-request"); 4 | const NotFoundError = require("../errors/not-found"); 5 | 6 | const errorHandlerMiddleware = (err,req,res,next) => { 7 | if(err instanceof CustomAPIError) { 8 | if(err instanceof NotFoundError) { 9 | res.render("error", {msg: err.message, status: err.statusCode}); 10 | } else { 11 | res.status(err.statusCode).json(err.message); 12 | } 13 | 14 | } else { 15 | res.render("error", { 16 | msg: "Internal Server Error", 17 | status: 500 18 | }); 19 | } 20 | } 21 | 22 | module.exports = errorHandlerMiddleware; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # URL Shortener Website 2 | Front-end developed using HTML/CSS/JS 3 | 4 | Back-end developed using Express.js and MongoDB 5 | 6 | ![Image of the app](https://i.ibb.co/mqT9F4J/Screenshot-41.png) 7 | ![Image of the app 2](https://i.ibb.co/LnxDhPG/Screenshot-42.png) 8 | 9 | ## Setup 10 | 11 | ```bash 12 | npm install && npm start 13 | ``` 14 | 15 | don't forget to add .env file contain the MONGO_URI, PORT and Base_URL Variables. 16 | 17 | (Base_URL is something like http://localhost:PORT) 18 | 19 | ## Functionality 20 | - URL Shortener Web App that makes generate short URL for your long URL 21 | - Generate QR Codes for your new short URL 22 | 23 | ## Future Upgrades 24 | - Add Auth and Login functionallity So each user can see his URLs. 25 | - Keep the insights (Visits and clicks) about each URL. 26 | 27 | ## Routers 28 | 29 | - main.js 30 | - url.js 31 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | require("express-async-errors") 3 | const express = require("express"); 4 | const connectDB = require("./db/connect"); 5 | const urlRouter = require("./routes/url"); 6 | const mainRouter = require("./routes/main"); 7 | const cors = require("cors"); 8 | const errorHandlerMiddleware = require("./middlewares/errorHandler"); 9 | const { json } = require("express"); 10 | const app = express(); 11 | app.set("view engine", "ejs"); 12 | app.use(cors()); 13 | app.use(express.json()); 14 | app.use(express.static("./public")); 15 | app.use("/api/v1/shorten",urlRouter); 16 | app.use("/",mainRouter); 17 | app.use(errorHandlerMiddleware); 18 | 19 | const port = 5000; 20 | const start = async () => { 21 | await connectDB(process.env.MONGO_URI) 22 | app.listen(port, console.log(`Server is listening on port ${port}`)); 23 | } 24 | 25 | start(); -------------------------------------------------------------------------------- /public/main.js: -------------------------------------------------------------------------------- 1 | const contentDiv = document.querySelector(".content") 2 | const form = document.querySelector("form"); 3 | const urlInput = document.querySelector("#long-url"); 4 | 5 | form.onsubmit = async (e) => { 6 | e.preventDefault(); 7 | try { 8 | const { data } = await axios.post("http://localhost:5000/api/v1/shorten", { 9 | longUrl: urlInput.value 10 | }); 11 | if (data.shortUrl) { 12 | contentDiv.innerHTML = `

Here is your short URL:

13 |
14 | ${data.shortUrl} 15 | 16 |
17 |
18 | 19 |
` 20 | } 21 | } catch (error) { 22 | console.log(error); 23 | } 24 | } -------------------------------------------------------------------------------- /controllers/url.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const {StatusCodes} = require("http-status-codes"); 3 | const BadRequestError = require("../errors/bad-request"); 4 | const validUrl = require("valid-url"); 5 | const shortid = require("shortid"); 6 | const Url = require("../models/Url"); 7 | const QRCode = require("qrcode"); 8 | const path = require("path"); 9 | 10 | const generateQRCode = async (shortId) => { 11 | if(!shortId) { 12 | throw new BadRequestError("Please provide the shortId!"); 13 | } 14 | try { 15 | await QRCode.toFile(`./QRCodes/${shortId}.png`, `${process.env.Base_URL}/${shortId}`, { 16 | errorCorrectionLevel: "H" 17 | }) 18 | } catch (error) { 19 | console.log(error); 20 | } 21 | } 22 | 23 | const getQRImage = async (req,res) => { 24 | const {shortId} = req.params; 25 | if(!shortId) { 26 | throw new BadRequestError("Please provide the shortId!") 27 | } 28 | const url = await Url.findOne({shortId}); 29 | if(!url) { 30 | throw new BadRequestError("Wanted Image doesn't exist!") 31 | } 32 | res.sendFile(path.join("C:/Users/asus/Desktop/WebProjects/MyUrlShortener", `/QRCodes/${shortId}.png`), err => { 33 | if(err) { 34 | console.log(err); 35 | } 36 | }) 37 | } 38 | 39 | const createShortenUrl = async (req,res) => { 40 | const {longUrl} = req.body; 41 | if(!longUrl || !validUrl.isUri(longUrl)) { 42 | throw new BadRequestError("Wrong url format!"); 43 | } 44 | try { 45 | let url = await Url.findOne({longUrl}); 46 | if(url) { 47 | generateQRCode(url.shortId); 48 | return res.status(StatusCodes.OK).json(url); 49 | } else { 50 | const shortId = shortid.generate(); 51 | const shortUrl = process.env.Base_URL + "/" + shortId; 52 | url = await Url.create({longUrl, shortUrl, shortId}); 53 | generateQRCode(shortId); 54 | return res.status(StatusCodes.CREATED).json(url); 55 | } 56 | } catch (error) { 57 | console.log(error); 58 | } 59 | } 60 | 61 | module.exports = {createShortenUrl, getQRImage} -------------------------------------------------------------------------------- /views/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | MyURLShortener 12 | 13 | 14 | 15 |
16 |
17 | 22 |
23 |
24 |

Shorten your looooong URLs like never before!

25 |

Copy your boring url. Paste it below. then boooom ! You got it, right?

26 |
27 |
28 | 29 |
30 | 31 | 32 |
33 |
34 |
35 |

Node JS Powered URL Shortener

36 |
37 | 50 |
51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap'); 2 | :root { 3 | --main-color: #FF621F; 4 | --secondary-color: #009DFF; 5 | --bg-color: #111827; 6 | --input-color: #374150; 7 | --input-placeholder-color: #8B909A; 8 | } 9 | 10 | * { 11 | box-sizing: border-box; 12 | } 13 | 14 | body { 15 | margin: 0; 16 | color: white; 17 | font-family: Roboto, sans-serif; 18 | } 19 | 20 | .container { 21 | background-color: var(--bg-color); 22 | min-height: 100vh; 23 | } 24 | 25 | header { 26 | padding: 30px; 27 | align-items: center; 28 | height: 10vh; 29 | } 30 | 31 | header .logo { 32 | display: flex; 33 | align-items: center; 34 | gap: 15px; 35 | margin-left: 50px; 36 | } 37 | 38 | header .logo h1 { 39 | color: var(--secondary-color); 40 | font-size: 40px; 41 | } 42 | 43 | header .logo i { 44 | color: var(--main-color); 45 | } 46 | 47 | header .logo .mobile-icon { 48 | display: none; 49 | } 50 | 51 | main { 52 | padding-top: 100px; 53 | display: flex; 54 | flex-direction: column; 55 | align-items: center; 56 | gap: 20px; 57 | height: 80vh; 58 | text-align: center; 59 | } 60 | 61 | main h1 { 62 | font-size: 55px; 63 | max-width: 60%; 64 | color: var(--main-color); 65 | } 66 | 67 | main h1 span { 68 | color: var(--secondary-color); 69 | } 70 | 71 | main p { 72 | font-size: 20px; 73 | } 74 | 75 | main .content { 76 | min-width: 50%; 77 | display: flex; 78 | gap: 15px; 79 | flex-direction: column; 80 | align-items: center; 81 | } 82 | 83 | main form { 84 | display: flex; 85 | flex-direction: column; 86 | gap: 10px; 87 | width: 100%; 88 | } 89 | 90 | main form label { 91 | align-self: flex-start; 92 | font-weight: 500; 93 | } 94 | 95 | main form .inputs { 96 | display: flex; 97 | gap: 5%; 98 | } 99 | 100 | main form .inputs input { 101 | height: 50px; 102 | padding: 15px; 103 | color: white; 104 | outline: none; 105 | border: 1px solid transparent; 106 | border-radius: 3px; 107 | } 108 | 109 | main form .inputs input[type="url"] { 110 | background-color: var(--input-color); 111 | width: 70%; 112 | } 113 | 114 | main form .inputs input[type="url"]:focus { 115 | border-color: var(--secondary-color); 116 | } 117 | 118 | main form .inputs input[type="submit"] { 119 | background-color: var(--main-color); 120 | cursor: pointer; 121 | font-weight: 500; 122 | width: 20%; 123 | min-width: min-content; 124 | transition: 0.4s; 125 | } 126 | 127 | main form .inputs input[type="submit"]:hover { 128 | border-color: var(--main-color); 129 | color: var(--main-color); 130 | background-color: white; 131 | } 132 | 133 | main form .inputs input[type="text"]::placeholder { 134 | color: var(--input-placeholder-color); 135 | font-size: 18px; 136 | } 137 | 138 | main > p:last-child { 139 | font-size: 15px; 140 | color: var(--input-placeholder-color); 141 | } 142 | 143 | main .content .box { 144 | background-color: var(--input-color); 145 | border-radius: 3px; 146 | padding: 10px; 147 | width: min-content; 148 | display: flex; 149 | align-items: center; 150 | gap: 10px; 151 | } 152 | 153 | main .content .box a { 154 | text-decoration: none; 155 | color: white; 156 | } 157 | 158 | main .content .box i { 159 | cursor: pointer; 160 | } 161 | 162 | footer { 163 | display: flex; 164 | align-items: center; 165 | justify-content: space-between; 166 | padding: 20px 100px; 167 | height: 10vh; 168 | } 169 | 170 | footer h1 { 171 | font-size: 25px; 172 | } 173 | 174 | footer .left { 175 | display: flex; 176 | align-items: center; 177 | gap: 15px; 178 | } 179 | 180 | footer .left i { 181 | color: var(--main-color); 182 | } 183 | 184 | footer .left h1 { 185 | color: var(--secondary-color); 186 | } 187 | 188 | footer .mid > h1 span:first-child { 189 | color: var(--main-color); 190 | } 191 | 192 | footer .mid > h1 span:nth-child(2) { 193 | color: var(--secondary-color); 194 | } 195 | 196 | footer .right { 197 | display: flex; 198 | gap: 15px; 199 | } 200 | 201 | footer .right a { 202 | text-decoration: none; 203 | color: white; 204 | } 205 | 206 | @media (max-width: 768px) { 207 | .container { 208 | min-height: auto; 209 | } 210 | 211 | header .logo { 212 | margin-left: 0; 213 | align-items: center; 214 | justify-content: center; 215 | } 216 | 217 | header .logo h1 { 218 | font-size: 25px; 219 | } 220 | 221 | header .logo .mobile-icon { 222 | display: block; 223 | } 224 | 225 | header .logo .desktop-icon { 226 | display: none; 227 | } 228 | 229 | main h1 { 230 | font-size: 40px; 231 | } 232 | 233 | footer, main, header { 234 | height: auto; 235 | } 236 | 237 | footer { 238 | flex-direction: column; 239 | padding: 20px; 240 | text-align: center; 241 | } 242 | 243 | main form .inputs { 244 | flex-direction: column; 245 | align-items: center; 246 | gap: 10px; 247 | } 248 | 249 | main .content { 250 | width: 90%; 251 | } 252 | 253 | main form label { 254 | align-self: center; 255 | } 256 | 257 | main form .inputs input[type="url"] { 258 | width: auto; 259 | min-width: 80%; 260 | } 261 | 262 | main form .inputs input[type="submit"] { 263 | width: auto; 264 | } 265 | } --------------------------------------------------------------------------------