DEV Community

Cover image for Want recruiters attention? Build this 🔥 project in ⌚ 5 minutes to 🚀 your portfolio!
Brandon
Brandon

Posted on

Want recruiters attention? Build this 🔥 project in ⌚ 5 minutes to 🚀 your portfolio!

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

Oldest comments (45)

Collapse
 
maxberko profile image
Max B

Hey Brandon,

Cool tutorial. How do you start the project locally ?

Thanks

Collapse
 
brandonkylebailey profile image
Brandon

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

Collapse
 
oscarmrom profile image
Oscar • Edited

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

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

Thanks for the comment imacchiavello ! 😄

Collapse
 
wannabehexagon profile image
ItsThatHexagonGuy

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

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

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

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
 
dostuffthatmatters profile image
Moritz Makowski

What is this? Why isn't there a 👎🏻 button?

Collapse
 
brandonkylebailey profile image
Brandon

Thanks for the comment Moritz! 😄 You'll have to take that up with dev.to 😅

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

Thanks for the feedback! Those are some solid suggestions and I'll definitely implement them! 😄

Collapse
 
whitewarrior profile image
White Warrior

Cool

Collapse
 
brandonkylebailey profile image
Brandon

Thank you!

Collapse
 
bennycode profile image
Benny Code

Cool project. If you don't mind I will create a TypeScript version of it. 😃

Collapse
 
brandonkylebailey profile image
Brandon

Thanks for commenting! Excited to see that!

Collapse
 
hoggworks profile image
Brian Hogg

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

Collapse
 
brandonkylebailey profile image
Brandon

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
 
andyinpembs profile image
andy • Edited

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

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

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 • Edited

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

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

Collapse
 
svda profile image
Sander van den Akker • Edited

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

Collapse
 
brandonkylebailey profile image
Brandon

Very true! Thanks for your comment Sander ! 🙌

Collapse
 
lyavale95 profile image
LyAVALE95

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

Collapse
 
brandonkylebailey profile image
Brandon

Haha no worries! Thanks for commenting 😃

Collapse
 
ng_update profile image
Kieran

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

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