So you're ready to start creating a portfolio but can't think of any ideas? Here's one AMAZING idea to demonstrate full stack skills and impress any potential employer! π₯
Getting started π
mkdir url-shortener
cd url-shortener
npm init -y
Here, we make a directory to store our project, and initialize it with npm.
Dependencies β οΈ
npm install dotenv express mongoose nanoid
We install a number of dependencies that we are going to use throughout this project:
- dotenv (Library utilizing environment variables)
- express (Express.js to create our server application)
- mongoose (ODM to store our URL's in our MongoDB database)
Folder setup πͺ
We need to make sure our project looks like this:
url-shortener/
βββ package.json
βββ client
βΒ Β βββ app.js
βΒ Β βββ index.html
βΒ Β βββ style.css
βββ server
βββ controllers
βΒ Β βββ url.controller.js
βββ index.js
βββ models
βΒ Β βββ url.model.js
βββ routes
βββ url.routes.js
We break our code in to routes, controllers and models. This makes code more maintainable through separation of concerns!
Server setup π!
Inside our server/index.js
file, add the following:
const express = require('express');
const mongoose = require('mongoose');
require('dotenv').config();
const urlRouter = require('./routes/url.routes');
const PORT = process.env.PORT || 8080;
const DB_URL = process.env.DB_URL || 'mongodb://localhost:27017/db';
const db = mongoose.connect(DB_URL, {
useCreateIndex: true,
useNewUrlParser: true,
useUnifiedTopology: true
}
).
then(res => res)
.catch(err => console.log(err));
const app = express();
app.use(express.json());
app.use(express.static('client'));
app.use('/url', urlRouter);
app.listen(PORT, () => {
console.log(`Server listening at http://localhost:${PORT}`);
});
Here, we import express and mongoose.
Then import out soon to be created router for handling our URL's.
Then initialize our database connection to store our data.
Next we create our express application and use our middleware (express.json(), express.static() and our router)
Creating the router β‘!
Inside our server/routes/url.routes.js
file, add the following:
const express = require('express');
const urlRoutes = express.Router();
const controller = require('../controllers/url.controller');
urlRoutes.get('/:slug',
controller.getUrl);
urlRoutes.post('/new',
controller.postUrl);
module.exports = urlRoutes;
Here, we import express and create an express router to attach our routes to.
Then, we import our controller to handle our requests when they have been called.
Lastly, we create our GET and POST requests to handle the retrieval and creation of our shortened URL's
Creating the controller β‘!
Now we need a controller to handle these routes!
Inside our server/controllers/url.controller.js
file, add the following:
const UrlModel = require('../models/url.model');
const {nanoid} = require('nanoid');
exports.getUrl = async (req, res) => {
const {slug} = req.params;
// check if slug exists
const foundSlug = await UrlModel.findOne({slug});
// if no slug exists, create one
if(!foundSlug || foundSlug.length == 0) {
let fullUrl = req.protocol + '://' + req.get('Host') + req.originalUrl;
res.status(404).json({message: "URL not found.", body:{slug, url: fullUrl}});
} else {
res.status(302).redirect(foundSlug.url);
}
}
exports.postUrl = async (req, res) => {
let {url, slug} = req.body;
// check if slug provided, create new one if not.
if(!slug) {
slug = nanoid(5);
}
slug = slug.toLocaleLowerCase();
// check if slug exists
const foundSlug = await UrlModel.find({slug});
// if no slug exists, create one
if(!foundSlug || foundSlug.length == 0) {
const newUrl = new UrlModel(
{
slug,
url
}
);
const response = await newUrl.save();
res.status(200).json({message: "Creation successful!", body:response});
} else {
res.status(409).json({message: "Resource already exists.", body:{slug: "", url:""}});
}
}
This is where we use our dependency nanoid.
What is nanoid?
nanoid is a library for generating small id strings. We are going to generate a small id string to use as our shortened URL!
The GET request π¦
The GET request retrieves the slug value from the get url :slug
and attempts to retrieve a matching entry from the database.
If a matching slug is found, then we redirect to the URL of the found slug.
If no slug is found, we notify the user with a 404 status that the desired URL was not found.
The POST request π¦
The POST request retrieves the url and slug from the POST request body, if no slug is provided, we use nanoid to generate a random slug of length 5.
This is so custom short URL's can be created by a user.
Example request:
POST http://localhost:8080/url/new HTTP/1.1
content-type: application/json
{
"slug": "abcde",
"url": "https://www.google.com"
}
This will create a URL of http://localhost:8080/abcde
Which redirects the user to https://www.google.com
We check to see if an entry already exists in the database with the desired slug.
If no entry exists, we save our new document to the database and return the created entry.
If a slug exists, we return a 409 response notifying the user the resource already exists.
The data model βοΈ!
The last thing to build out for our backend is the data model that mongoose will use for our MongoDB database.
Inside our server/models/url.model.js
file, add the following:
const mongoose = require('mongoose');
const UrlModel = mongoose.model('Url',
mongoose.Schema(
{
slug: {
type: String,
minlength: [5, 'Slug does not contain enough characters (Minimum 5).'],
maxlength: [5, 'Slug contains too many characters (Maximum 5).'],
trim: true,
validate: {
validator : (slug) => {
return /[\w\-]/.test(slug);
},
message: props => `${props.value} is not a valid slug.`
}
},
url: {
type: String,
required: [true, 'A valid URL must be provided.'],
trim: true
}
},
{timestamps: true}
)
);
module.exports = UrlModel;
Inside this script, we first import mongoose to use to create our mongoose model.
Then we create a UrlModel Schema with two parameters:
- slug (A string value of the shortened URL)
- url (A string value of the URL to redirect to)
We create some basic validation for the slug using regex to ensure that the slug only contains alphanumeric characters along with hyphens (-).
And that is the backend complete π! time to build out our frontend!
The Frontend π!
Our client directory should contain the following files:
client/
βββ app.js
βββ index.html
βββ style.css
The index.html file π
Inside our index.html
file, add the following form:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<title>MicroUrl</title>
</head>
<body>
<main>
<h1>MicroUrl</h1>
</main>
<section>
<form action="javascript:;" onsubmit="createUrl(displayResponse)">
<label for="url">Url to shorten:</label>
<input type="url" name="url" id="url" required>
<label for="slug">Optional. Custom micro url:</label>
<input type="text" name="slug" id="slug">
<input type="submit" value="Create">
</form>
</section>
<section id="response">
</section>
<script src="app.js"></script>
</body>
</html>
Our form contains two inputs (one for our URL to shorten and one for a potential custom slug)
The style.css file π
Inside our style.css
file, add the following form:
body {
margin-top: 20vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #84613D;
font-family: "Lucida Console", Monaco, monospace;
background: #FDF9EA;
}
body > * {
width: 40vw;
height: auto;
}
form {
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
margin: 1rem 0;
}
form > * {
margin: .5rem 0;
padding: 1rem;
}
form > button {
padding: 0;
}
Our site should now contain an attractive, responsive form!
The last thing to do is to add the Javascript to create our URL and display a response!
The app.js file π
Inside our app.js
file, add the following form:
const createUrl = async (callback=null) => {
this.preventDefault;
let response = await fetch('/url/new', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(
{
url:this.url.value,
slug:this.slug.value
})
});
let result = await response.json();
console.log(result);
if(callback) {
callback("response", result);
}
}
const displayResponse = (elementKey, data) => {
const {message, body} = data;
const parentElement = document.getElementById(elementKey);
parentElement.innerHTML = "";
let divElement = document.createElement('div');
let pElement = document.createElement('p');
pElement.appendChild(document.createTextNode(message));
let aElement = document.createElement('a');
if(body.slug) {
aElement.appendChild(document.createTextNode(`${window.location.href}url/${body.slug}`));
aElement.href = `${window.location.href}url/${body.slug}`;
} else {
aElement.appendChild(document.createTextNode(""));
}
divElement.appendChild(pElement);
divElement.appendChild(aElement);
parentElement.appendChild(divElement);
}
We have two functions:
- createUrl
- displayReponse
createUrl
accepts a callback as an argument to execute after it has handled the submit of this form.
This can be referred to as the callback design pattern
Our createUrl function uses fetch
to POST a request to our server with the form data. Once complete we use our displayResponse function to display the newly created shortened URL:
Summary π
If you made is this far congratulations! π
You've learned a great deal in this project. API creation, data validation, frontend design. You should now be well on your way to creating a π₯ portfolio!
If you enjoyed this tutorial, feel free to give me a follow and check out some of my social media!
Twitter
Github
Top comments (45)
It'll take just 5 minutes if you copy/paste everything from the post
Might as well fork it and call it yours
Thanks for the comment Nico π this project isn't hosted on my Github at this time!
Donβt get me wrong, I love the content, it just feels like faking it. If I were to put this and get attention by a hiring manager, Iβd be lying if I said I knew how I got to make this to work lol
Not at all! I appreciate that! You're completely right! the idea is to get something built as quickly as possible and use that to develop and expand their (who ever builds this) knowledge. When i first learned how to code it took such a long time for me to understand concepts and tools because i just didn't build any real life projects!
Even so, it won't. Setting up mongodb, installing dependencies... 20 minutes at least. :|
Thanks for the comment Mydrax! It might take just 5 minutes if you're a fast reader π
I think you should expand your opening paragraph to make it clearer what is being built with this project. You can guess it's a URL shortener with "mkdir url-shortener", but it would be good if this was written explicitly, and also a few words on the features/functionalities that will be implemented.
Thanks for the feedback! Those are some solid suggestions and I'll definitely implement them! π
Nice example code with comments, but I think you forgot to mention what the heck you are building. Also, it looks like you didn't include how to set up the MongoDB database.
Thanks for the comment z2lai! You're right, i could have made it more clear in the introduction what was being built. It is mentioned in the project set up section however π And you're right! my previous blog post mentions this! hope that helps π
I think it would be helpful to link to your previous post for people that want to run the entire application you walk us through. Well done!
That's true! I'll make sure to improve things with my next blog post. Thanks for the feedback! π
On my browser, there's a weirdly huge graphic repeated after every step; anyone else seeing that?
I saw it in Vivaldi so pasted the url into chrome where it seems fine.
Tried to upload an image but for some reason that's not working...
Hey Brian! Thanks for leaving a comment! Can you share what this graphic looks like? What browser are you using? Thanks ! π
It seems like it's looking fine now; maybe there was a dev.to issue that got resolved, I didn't take a screenshot when I noticed the issue, unfortunately. The image I saw was black with white braces, sort of like an icon you might use around a QR code in an icon to denote an ability to scan, but with nothing inside it.
The 1st time I load this page, I experienced the same thing. After I reloaded, everything was fine.
I like this project. It's very similar to a freeCodeCamp project in the API and Microservices course, which is one of the certificates I enjoyed the most.
I've been thinking of going back to my answer for that activity and personalising it. I might just do it after reading this. Thank you OP.
Hey Brandon,
Cool tutorial. How do you start the project locally ?
Thanks
Great question Max! Simple run
node server/index.js
in a terminal from the root of your project!Including this in the article may be helpful along with how to run the frontend on a browser (i.e. In your browser address bar navigate to "localhost:8080/. I've learned that we should assume readers know the bare minimum and I think your guide does a good job so far. Just a couple more bits of information for the newcomers.
Hey Oscar thanks for commenting! That's a very good point and I appreciate the feedback! I'm going to take all this on board for my future posts and hopefully improve! π€―
"Want recruiters attention ?"
Do you think this will be enough when the recruiters want to see some skills in React/Angular/Vue ? We know a lot of job requirements state this thing among other things.
For me, using the same project, in order to enhance impression for them, I may replace all related codes in front end with codes written in React + Bootstrap 4.
As for backend :
Thanks for sharing your idea and approach in creating a portfolio.
Thank you for the tutorial brandon.
Thanks for the comment imacchiavello ! π
Very nice. In order to really impress a recruiter you could consider TDD and add some unit tests.
Very true! Thanks for your comment Sander ! π
I think this is a great, fun, project. Would you mind if I port it to .Net and make it available on GitHub?
Go ahead!
Hi Brandon,
It took me a while longer than expected, but I finally have a working port of your app in .Net Core. I am planning on a series of posts to describe my project. I also made my source code available on GitHub. Would you mind linking to my Dev.to post and GitHub repo? Here are the URLs: dev.to/pdelco/a-reference-applicat... and github.com/pdelco/m-url for generating shortened URLs
I appreciate it!