Building URL Shortener with MongoDB, Express Framework And TypeScript
Tags: typescript, mongodb, javascript, express
Published: 2019-11-10 09:32:50

*This post was first [published on my blog](https://aligoren.com/building-url-shortener-with-mongodb-express-framework-and-typescript/).* 

Hi, in the last post I published, I [talked](https://dev.to/aligoren/developing-an-express-application-using-typescript-3b1 "talked") about Express Framework and TypeScript. In this post, I'll use that structure.

So, I won't talk about what structure we will use.

## Before Starting

Before we starting, we'll use MongoDB for this project and to get environment variable values, we'll use the dotenv package.

**nodemon**:  [Nick Taylor](https://dev.to/nickytonline) suggested to me. Using nodemon you don't need to stop-start your applications. It's already doing this for you.

**mongoose**: A driver to connect MongoDB.

**dotenv**: A package to get environment variable values.

### Install Packages

```bash
npm i typescript nodemon express mongoose pug ts-node dotenv @types/node @types/mongoose @types/express
```

Let's edit the **scripts** section in the **package.json** file.

```json
"scripts": {
  "dev": "nodemon src/server.ts",
  "start": "ts-node dist/server.js",
  "build": "tsc -p ."
}
```

**tsconfig.json**

```json
{
    "compilerOptions": {
        "sourceMap": true,
        "target": "es6",
        "module": "commonjs",
        "outDir": "./dist",
        "baseUrl": "./src"
    },
    "include": [
        "src/**/*.ts"
    ],
    "exclude": [
        "node_modules"
    ]
}
```

Let's create a project structure

#### public

##### css

In this folder, we will have two CSS files named **bootstrap.css** and **app.css**. In bootstrap.css file, we'll be used bootstrap 4.x. And the app.css file we'll be used for custom styles.

**app.css**

```css
.right {
    float: inline-end;
}
```

##### js

In this folder, we will have a file named app.js. Client-side operations will be here.


**app.js**

```js
const btnShort = document.getElementById('btn-short')
const url = document.getElementById('url')
const urlAlert = document.getElementById('url-alert')
const urlAlertText = document.getElementById('url-alert-text')

const validURL = (str) => {
    const pattern = new RegExp('^(https?:\\/\\/)?'+
      '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+
      '((\\d{1,3}\\.){3}\\d{1,3}))'+
      '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+
      '(\\?[;&a-z\\d%_.~+=-]*)?'+
      '(\\#[-a-z\\d_]*)?$','i');

    return !!pattern.test(str);
}

function saveClipBoard(data) {
    var dummy = document.createElement('input');
    var text = data;

    document.body.appendChild(dummy);
    dummy.value = text;
    dummy.select();
    var success = document.execCommand('copy');
    document.body.removeChild(dummy);
    
    return success;
}

const shortenerResponse = (isValidUrl, serverMessage) => {

    let message = ''

    if (isValidUrl) {
        urlAlert.classList.remove('alert-danger')
        urlAlert.classList.add('alert-success')
        urlAlert.classList.remove('invisible')

        message = `
            <strong>Your URL:</strong> 
            <a id="shorted-url" href="${serverMessage}" target="_blank">${serverMessage}</a>
            <button class="btn btn-sm btn-primary right" id="btn-copy-link">Copy</button>
            <span class="mr-2 right d-none" id="copied">Copied</span>
            
        `
    } else {
        urlAlert.classList.remove('alert-success')
        urlAlert.classList.add('alert-danger')
        urlAlert.classList.remove('invisible')

        message = `<strong>Warning:</strong> ${serverMessage}`
    }

    urlAlertText.innerHTML = message
}

url.addEventListener('keypress', (e) => {
    if (e.which == 13 || e.keyCode == 13 || e.key == 'Enter') {
        btnShort.click()
    }
})

btnShort.addEventListener('click', async () => {
    
    const longUrl = url.value

    const isValidUrl = validURL(longUrl)
    
    if(isValidUrl) {
        const response = await fetch('/create', {
            method: 'POST',
            body: JSON.stringify({
                url: longUrl
            }),
            headers: {
                'Content-Type': 'application/json'
            }
        }).then(resp => resp.json())

        let success = response.success
        let message = '' 

        if(success) {
            const { url } = response
            message = `${window.location.origin}/${url}`
        } else {
            message = `URL couldn't shortened`
        }

        shortenerResponse(success, message)

        
    } else {
        shortenerResponse(isValidUrl, 'Please enter a correct URL')
    }    
})

document.addEventListener('click', (e) => {
    if (e.target && e.target.id == 'btn-copy-link') {
        const shortedUrl = document.getElementById("shorted-url")

        const isCopied = saveClipBoard(shortedUrl.href)

        if (isCopied) {
            document.getElementById('copied').classList.remove('d-none')
        }

    }
    
})
```

### src

#### controllers

In this folder, we'll have controllers and their model and interface files.

##### controllers/shortener.controller.ts

In this controller, we will insert a long URL to the Mongo Database. By the way, we didn't have a MongoDB connection yet.

**generateRandomUrl**: A private method to generate random characters. It expects a character length number.

**index**: An async method to show index page.

**get**: An async method to get short URL information. It expects shortcode as a parameter. Like: `http://example.com/abc12`

**create**: An async method to short long URL. Firstly, it looks up the long URL. If it exists, it will show the shortcode in the MongoDB.

Using **shortenerModel** we can save documents to MongoDB and search in MongoDB.

```typescript
import * as express from 'express'
import { Request, Response } from 'express'
import IControllerBase from 'interfaces/IControllerBase.interface'

import shortenerModel from './shortener.model'
import IShortener from './shortener.interface';


class ShortenerController implements IControllerBase {
    public path = '/'
    public router = express.Router()
    
    constructor() {
        this.initRoutes()
    }

    public initRoutes() {
        this.router.get('/', this.index)
        this.router.get('/:shortcode', this.get)
        this.router.post('/create', this.create)
    }

    private generateRandomUrl(length: Number) {

        const possibleChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

        let urlChars = "";

        for (var i = 0; i < length; i++) {
            urlChars += possibleChars.charAt(Math.floor(Math.random() * possibleChars.length));
        }
            
        return urlChars;
    }

    index = async(req: Request, res: Response) => {

        res.render('home/index')
    }

    get = async(req: Request, res: Response) => {
        
        const { shortcode } = req.params

        const data: IShortener = {
            shortUrl: shortcode
        }

        const urlInfo = await shortenerModel.findOne(data)

        if (urlInfo != null) {
            res.redirect(302, urlInfo.longUrl)
        } else {
            res.render('home/not-found')
        }
    }

    create = async(req: express.Request, res: express.Response) => {

        const { url } = req.body

        const data: IShortener = {
            longUrl: url
        }

        let urlInfo = await shortenerModel.findOne(data)

        if (urlInfo == null) {
            const shortCode = this.generateRandomUrl(5)

            const shortData: IShortener = {
                longUrl: url,
                shortUrl: shortCode
            }
    
            const shortenerData = new shortenerModel(shortData)
    
            urlInfo = await shortenerData.save()
        }

        res.json({
            success: true,
            message: 'URL Shortened',
            url: urlInfo.shortUrl
        })

        
    }
}

export default ShortenerController
```

##### controllers/shortener.interface.ts

In this interface, we're using an interface named ISHortener. It has two optional parameters.

```typescript
interface IShortener {
    longUrl?: string,
    shortUrl?: string
}

export default IShortener
```

##### controllers/shortener.model.ts

In this file, we're building a mongoose schema. It has two optional parameters such as **shortener.interface.ts**. Also, this model expects IShortener.

```typescript
import * as mongoose from 'mongoose'
import IShortener from './shortener.interface'

const shortenerSchema = new mongoose.Schema({
    longUrl: String,
    shortUrl: String
})

const shortenerModel = mongoose.model<IShortener & mongoose.Document>('Shortener', shortenerSchema);
 
export default shortenerModel;
```

#### interfaces

In this folder, we'll only have one interface file. That will be **IControllerBase**.

##### interfaces/IControllerBase.interface.ts

```typescript
interface IControllerBase {
    initRoutes(): any
}

export default IControllerBase
```

#### middleware

There is nothing here, we have created this folder, in case you need middleware.

#### src/app.ts

In this file, we'll connect to the MongoDB. We're also using **dotenv** to get environment variables.

**initDatabase**: We're connecting MongoDB here.

```typescript
import * as express from 'express'
import { Application } from 'express'
import * as mongoose from 'mongoose';
import 'dotenv/config';


class App {
    public app: Application
    public port: number

    constructor(appInit: { port: number; middleWares: any; controllers: any; }) {
        this.app = express()
        this.port = appInit.port

        this.initDatabase()
        this.middlewares(appInit.middleWares)
        this.routes(appInit.controllers)
        this.assets()
        this.template()
    }

    private middlewares(middleWares: { forEach: (arg0: (middleWare: any) => void) => void; }) {
        middleWares.forEach(middleWare => {
            this.app.use(middleWare)
        })
    }

    private routes(controllers: { forEach: (arg0: (controller: any) => void) => void; }) {
        controllers.forEach(controller => {
            this.app.use('/', controller.router)
        })
    }

    private initDatabase() {
        const {
            MONGO_USER,
            MONGO_PASSWORD,
            MONGO_PATH
        } = process.env

        mongoose.connect(`mongodb+srv://${MONGO_USER}:${MONGO_PASSWORD}${MONGO_PATH}`, { 
            useCreateIndex: true,
            useNewUrlParser: true,
            useFindAndModify: false, 
            useUnifiedTopology: true
        })
    }

    private assets() {
        this.app.use(express.static('public'))
        this.app.use(express.static('views'))
    }

    private template() {
        this.app.set('view engine', 'pug')
    }

    public listen() {
        this.app.listen(this.port, () => {
            console.log(`App listening on the http://localhost:${this.port}`)
        })
    }
}

export default App
```

#### src/server.ts

This is a file to serve the application.

```typescript
import App from './app'
import * as bodyParser from 'body-parser'
import ShortenerController from './controllers/shortener/shortener.controller'

const app = new App({
    port: 5000,
    controllers: [
        new ShortenerController()
    ],
    middleWares: [
        bodyParser.json(),
        bodyParser.urlencoded({ extended: true }),
    ]
})

app.listen()
```

### views

In this folder, we'll have view files.

#### views/home/home.pug

```pug
<!DOCTYPE html>
html(lang="en")
    head
        meta(charset="UTF-8")
        meta(name="viewport", content="width=device-width, initial-scale=1.0")
        meta(http-equiv="X-UA-Compatible", content="ie=edge")
        link(rel="stylesheet", href="css/bootstrap.css")
        link(rel="stylesheet", href="css/app.css")
        title TypeScript URL Shortener!
    body
        main(class="container")
            div(class="jumbotron")
                div(class="row")
                    div(class="col-md-12 align-self-center")
                        h1(class="text-center") URL Shortener
                        label(for="url") URL
                        div(class="input-group")
                            input.form-control(type="text", id="url", role="url", aria-label="Short URL")
                            div(class="input-group-append")
                                button(class="btn btn-md btn-danger", id="btn-short", role="button", aria-label="Short URL Button") Short URL
                
                div(class="row")
                    div(class="col-md-12")
                        div(class="alert alert-danger invisible mt-3", id="url-alert" role="alert")
                            span(id="url-alert-text") URL shorthened


        footer(class="footer")
            div(class="container")
                span(class="text-muted") TypeScript URL Shortener!
        
        script(src="js/app.js")
```

### MongoDB

To connect MongoDB, we need to have a MongoDB server. Instead of install a new MongoDB server, we'll use [MongoDB Cloud](https://cloud.mongodb.com/). There is a Free Tier. You don't need to pay for it.

After you created an account, your cluster will be preparing. There are somethings you have to do. The first one, you need to create a database user.

![MongoDB Admin](https://thepracticaldev.s3.amazonaws.com/i/1ju4933j3s9q5vajqsez.png)

The last thing you have to do, you need to give IP permission. In the MongoDB cloud, you have to do that.

![MongoDB Network](https://thepracticaldev.s3.amazonaws.com/i/uueq1dixtz1ppnntmddd.png)

### .env

In this file, we'll have MongoDB information;

```env
MONGO_USER=YOUR MONGO USERNAME
MONGO_PASSWORD=YOUR MONGO PASSWORD
MONGO_PATH=YOUR MONGO DATABASE URL
```

That's all. Let's run the application :)

```bash
npm run dev
```

### Screenshot

![URL Shortener Screenshot](https://thepracticaldev.s3.amazonaws.com/i/82ow0pqjd5ir2omsk96k.png)

### Conclusion

This was an excellent experience for me. I really loved TypeScript and Express with MongoDB.

**GitHub**: [https://github.com/aligoren/ts-url-shortener](https://github.com/aligoren/ts-url-shortener)