loading...
Cover image for Want recruiters attention? Build this πŸ”₯ project in ⌚ 5 minutes to πŸš€ your portfolio!

Want recruiters attention? Build this πŸ”₯ project in ⌚ 5 minutes to πŸš€ your portfolio!

brandonkylebailey profile image Brandon ・6 min read

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
Enter fullscreen mode Exit fullscreen mode

Here, we make a directory to store our project, and initialize it with npm.

Dependencies ⚠️

npm install dotenv express mongoose nanoid
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:""}});
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Our site should now contain an attractive, responsive form!
Alt Text

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);
}
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

Once submitted:
Alt Text

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

Discussion

pic
Editor guide
Collapse
xyn profile image
Mydrax

It'll take just 5 minutes if you copy/paste everything from the post

Collapse
nlabrad profile image
Nico

Might as well fork it and call it yours

Collapse
brandonkylebailey profile image
Brandon Author

Thanks for the comment Nico πŸ˜„ this project isn't hosted on my Github at this time!

Thread Thread
nlabrad profile image
Nico

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

Thread Thread
brandonkylebailey profile image
Brandon Author

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!

Collapse
brandonkylebailey profile image
Brandon Author

Thanks for the comment Mydrax! It might take just 5 minutes if you're a fast reader πŸ˜„

Collapse
fomenkogregory profile image
Greg

Even so, it won't. Setting up mongodb, installing dependencies... 20 minutes at least. :|

Collapse
drcat7 profile image
Cat

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.

Collapse
brandonkylebailey profile image
Brandon Author

Thanks for the feedback! Those are some solid suggestions and I'll definitely implement them! πŸ˜„

Collapse
z2lai profile image
z2lai

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.

Collapse
brandonkylebailey profile image
Brandon Author

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 πŸ˜„

Collapse
oscarmrom profile image
Oscar

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!

Thread Thread
brandonkylebailey profile image
Brandon Author

That's true! I'll make sure to improve things with my next blog post. Thanks for the feedback! πŸ˜‡

Collapse
andyst81 profile image
andyst81

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.

Collapse
hoggworks profile image
Brian Hogg

On my browser, there's a weirdly huge graphic repeated after every step; anyone else seeing that?

Collapse
andyinpembs profile image
andy

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...

Collapse
brandonkylebailey profile image
Brandon Author

Hey Brian! Thanks for leaving a comment! Can you share what this graphic looks like? What browser are you using? Thanks ! πŸ˜„

Collapse
hoggworks profile image
Brian Hogg

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.

Collapse
lexiebkm profile image
Alexander B.K.

The 1st time I load this page, I experienced the same thing. After I reloaded, everything was fine.

Collapse
maxberko profile image
Max B

Hey Brandon,

Cool tutorial. How do you start the project locally ?

Thanks

Collapse
brandonkylebailey profile image
Brandon Author

Great question Max! Simple run node server/index.js in a terminal from the root of your project!

Collapse
oscarmrom profile image
Oscar

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.

Thread Thread
brandonkylebailey profile image
Brandon Author

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! 🀯

Collapse
imacchiavello profile image
imacchiavello

Thank you for the tutorial brandon.

Collapse
brandonkylebailey profile image
Brandon Author

Thanks for the comment imacchiavello ! πŸ˜„

Collapse
lexiebkm profile image
Alexander B.K.

"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 :

  1. I will use mySQL, with or without ORM, instead of MongoDB.
  2. Beside Express, I will probably try Hapi too. So there can two code versions : one written with Express, the other with Hapi. One thing that I like in your approach is that you use MVC pattern, although only model and controller. I am quite familiar with this architecture pattern, because in my current real project (not in github), I use PHP + Laravel. I am relatively new in Node.js, and actually still learning, so maybe I will write this Node.js project in github. Of course I will try it locally before deployment.

Thanks for sharing your idea and approach in creating a portfolio.

Collapse
kieran815 profile image
Kieran815

Nice Project. It reminds me of the Url Shortener on FreeCodeCamp. This took me MAYBE in minutes real-time to crank out, with typing and adding notes. The title is "Build it in 5 minutes", not "Hold my Hand as I walk you through the finer points of Express", so I'm not sure why people are complaining that you don't go into explicit detail regarding setup...
If you can't kick this little app out quickly, you're probably not ready to talk to recruiters.

Collapse
brandonkylebailey profile image
Brandon Author

Thanks for commenting Kieran!

I actually haven't seen that one so I will have to check it out!

I appreciate the support! I think many people have viewed the post with different expectations of how much support this post would provide for the more junior devs out there! Which is totally valid! I'm working to maintain a balance to not bore the more experienced viewers whilst still being able to provide value to the juniors! πŸ’©

Collapse
pdelco profile image
Paul Delcogliano

I think this is a great, fun, project. Would you mind if I port it to .Net and make it available on GitHub?

Collapse
brandonkylebailey profile image
Brandon Author

Go ahead!

Collapse
brojenuel profile image
Jenuel Oras Ganawed

wow, this was a nice Idea. Thanks, @brandonkylebailey , More content like this is very useful. Create more sample projects where people will learn new Ideas.

Collapse
leomjaques profile image
Leonardo J. πŸ‘¨πŸ»β€πŸ’»

I'd first put what we are building at the top of the post. I only figured it out when at the bottom of it, with the images. I will give it a try!

Collapse
willem640 profile image
willem640

Cool!

Collapse
lyavale95 profile image
LyAVALE95

5 minutes to git clone all the stuff (kidding, thanks!)

Collapse
brandonkylebailey profile image
Brandon Author

Haha no worries! Thanks for commenting πŸ˜ƒ

Collapse
svda profile image
Sander van den Akker

Very nice. In order to really impress a recruiter you could consider TDD and add some unit tests.

Collapse
brandonkylebailey profile image
Brandon Author

Very true! Thanks for your comment Sander ! πŸ™Œ

Collapse
webdevinci profile image
webdevinci

Very nice. Add the host name for 'm.url/' (micro url) to point to 'localhost:8080/' to make it more 'micro'

Collapse
brandonkylebailey profile image
Brandon Author

Hey webdevinci! Thanks for the comment! that is something i wasn't aware of and really like actually! hopefully someone else sees this too! thanks! πŸ˜„

Collapse
whitewarrior profile image
Collapse
brandonkylebailey profile image
Brandon Author

Thank you!

Collapse
bennycode profile image
Benny Neugebauer

Cool project. If you don't mind I will create a TypeScript version of it. πŸ˜ƒ

Collapse
brandonkylebailey profile image
Brandon Author

Thanks for commenting! Excited to see that!

Collapse
dostuffthatmatters profile image
Moritz Makowski

What is this? Why isn't there a πŸ‘ŽπŸ» button?

Collapse
brandonkylebailey profile image
Brandon Author

Thanks for the comment Moritz! πŸ˜„ You'll have to take that up with dev.to πŸ˜…