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

Latest comments (45)

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

Go ahead!

Collapse
 
pdelcogliano profile image
Paul Delcogliano

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!

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
 
leomjaques profile image
leo 👨🏻‍💻

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
 
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
 
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
 
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
 
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
 
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
 
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
 
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
 
hoggworks profile image
Brian Hogg

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

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
 
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
 
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
 
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
 
whitewarrior profile image
White Warrior

Cool

Collapse
 
brandonkylebailey profile image
Brandon

Thank you!

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