This post was first published on my blog.
Hi, in the last post I published, I 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 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
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.
"scripts": {
"dev": "nodemon src/server.ts",
"start": "ts-node dist/server.js",
"build": "tsc -p ."
}
tsconfig.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
.right {
float: inline-end;
}
js
In this folder, we will have a file named app.js. Client-side operations will be here.
app.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.
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.
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.
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
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.
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.
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
<!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. 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.
The last thing you have to do, you need to give IP permission. In the MongoDB cloud, you have to do that.
.env
In this file, we'll have MongoDB information;
MONGO_USER=YOUR MONGO USERNAME
MONGO_PASSWORD=YOUR MONGO PASSWORD
MONGO_PATH=YOUR MONGO DATABASE URL
That's all. Let's run the application :)
npm run dev
Screenshot
Conclusion
This was an excellent experience for me. I really loved TypeScript and Express with MongoDB.
Top comments (5)
Thx for sharing.
I would say nowadays there is a lot of potential for reducing the amount of resources required for such a service.
Especially when "going cloud" any services that need to run 24/7 are expensive and a proper production MongoDB setup (in case sth needs to scale) costs quite an amount of money in the cloud.
URL collision should be checked ?
Yes, I think it must be checked. But in this example, I didn’t because I didn’t want to long post 🤓
It's just another 4 lines of code.... I think you should add it
Ah, I didn't update the post. Someone sent a pull request to repo. I will add :)