├── .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 | 
7 | 
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 |
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 |
23 |
24 | Shorten your looooong URLs like never before!
25 | Copy your boring url. Paste it below. then boooom ! You got it, right?
26 |
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 | }
--------------------------------------------------------------------------------