Want recruiters attention? Build this 🔥 project in ⌚ 5 minutes to 🚀 your portfolio!
Tags: tutorial, beginners, webdev, javascript
Published: 2020-10-26 11:21:03

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! :boom:

# Getting started :rocket:
```c
mkdir url-shortener
cd url-shortener
npm init -y
```
Here, we make a directory to store our project, and initialize it with **npm**.

# Dependencies :warning:
```c
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 :circus_tent:

We need to make sure our project looks like this:
```c
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 :cyclone:!

Inside our `server/index.js` file, add the following:
```c
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 :zap:!

Inside our `server/routes/url.routes.js` file, add the following:
```c
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 :zap:!

Now we need a controller to handle these routes!

Inside our `server/controllers/url.controller.js` file, add the following:
```c
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 :bird:

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

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:
```c
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 :snowflake:!

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:
```c
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 :clap:! time to build out our frontend!*

# The Frontend :eyes:!

Our client directory should contain the following files:
```c
client/
├── app.js
├── index.html
└── style.css
```
## The index.html file :skull:

Inside our `index.html` file, add the following form:
```c
<!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 :thought_balloon:

Inside our `style.css` file, add the following form:
```c
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!
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ck7fer7mm16wmus0jwmw.JPG)

The last thing to do is to add the Javascript to create our URL and display a response!

## The app.js file :see_no_evil:

Inside our `app.js` file, add the following form:
```c
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:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/fvaxp4109un1ehdnogmg.JPG)

Once submitted:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/xo63msxkvztevcjkgncd.JPG)

# Summary :pray:

If you made is this far congratulations! :tada: 
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 :fire: portfolio!

*If you enjoyed this tutorial, feel free to give me a follow and check out some of my social media!*
[Twitter](https://twitter.com/brandonkpbailey)
[Github](https://github.com/brandon-kyle-bailey)