├── .eslintrc.json ├── .gitignore ├── .nowignore ├── LICENSE.md ├── README.md ├── back ├── handlers │ ├── chop.js │ └── redirect.js ├── helpers │ ├── short.js │ └── uuid.js ├── middlewares │ ├── acceptedContentType.js │ ├── errorHandler.js │ └── responseTime.js └── models │ └── Short.js ├── front ├── index.html ├── scripts.js └── styles.css ├── now.json └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "globals": { 4 | "fetch": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /.nowignore: -------------------------------------------------------------------------------- 1 | *.md 2 | .eslitrc* 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 paulogdm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chop My Url! 2 | 3 | A shortener URL example deployed to [Now 2.0](https://now.sh/). 4 | 5 | Live demo: [chop.now.sh](https://chop.now.sh). The source and logs can be accessed by attaching `/_src` or `/_logs`, respectively. 6 | 7 | ## Getting Started 8 | 9 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on [Now](https://now.sh/). 10 | 11 | ### Prerequisites 12 | 13 | To run this example locally, you need [NodeJS](https://nodejs.org/). 14 | 15 | Testing each API endpoint individually: 16 | 17 | ```shell 18 | $ npm i 19 | $ mongo_chop_user=yourdbuser mongo_chop_passnode=yourdbpass node back/handlers/chop.js 20 | $ mongo_chop_user=yourdbuser mongo_chop_passnode=yourdbpass node back/handlers/redirect.js 21 | ``` 22 | 23 | ## Running the tests 24 | 25 | Tests can be run by executing: 26 | 27 | ``` 28 | npm run test 29 | ``` 30 | 31 | ## Deployment 32 | 33 | To deploy this example, you need to install `now-cli`: 34 | 35 | ``` 36 | $ npm i -g now 37 | ``` 38 | 39 | Then, simply: 40 | 41 | ``` 42 | $ now 43 | ``` 44 | 45 | ## Built With 46 | 47 | * [Koa](https://koajs.com/) - Next gen web framework for Node 48 | * [Milligram](https://milligram.io/) - A minimalist CSS framework 49 | * [Mongoose](https://mongoosejs.com/) - Elegant mongodb object modeling for node.js 50 | 51 | ## License 52 | 53 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 54 | -------------------------------------------------------------------------------- /back/handlers/chop.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const koaBody = require('koa-body') 3 | 4 | const responseTime = require('../middlewares/responseTime') 5 | const errorHandler = require('../middlewares/errorHandler') 6 | const acceptedContentType = require('../middlewares/acceptedContentType') 7 | 8 | const shortActions = require('../helpers/short') 9 | 10 | const app = new Koa() 11 | app.use(koaBody({ jsonLimit: '1kb' })) 12 | app.use(responseTime) 13 | app.use(errorHandler.use) 14 | app.on('Error', errorHandler.on) 15 | app.use(acceptedContentType) 16 | 17 | const main = async ctx => { 18 | const { originalUrl } = ctx.request.body 19 | // mongo insert 20 | const { shortPath } = await shortActions.insert(originalUrl) 21 | // ctx.request.origin may no be suited for all cases 22 | // but here we glue the path with the origin url: 23 | // eg: https://chop.now.sh + / + abcdefghij 24 | const shortUrl = `${ctx.request.origin}/${shortPath}` 25 | ctx.body = { originalUrl, shortUrl } 26 | } 27 | 28 | app.use(main) 29 | 30 | // to test locally, is there a better way? 31 | if (!process.env.NOW_REGION) app.listen(3000) 32 | module.exports = app.callback() 33 | -------------------------------------------------------------------------------- /back/handlers/redirect.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const responseTime = require('../middlewares/responseTime') 3 | const errorHandler = require('../middlewares/errorHandler') 4 | const shortActions = require('../helpers/short') 5 | 6 | const app = new Koa() 7 | 8 | app.use(responseTime) 9 | app.use(errorHandler.use) 10 | app.on('Error', errorHandler.on) 11 | 12 | /** 13 | * Function that handles a failed request for a short url 14 | * @param {Object} ctx Koa's context 15 | */ 16 | const notFound = async ctx => { 17 | ctx.status = 404 18 | } 19 | 20 | /** 21 | * This function redirects the request to another location 22 | * @param {Object} ctx Koa's context 23 | * @param {String} originalUrl URL stored in mongo 24 | */ 25 | const redirect = async (ctx, originalUrl) => { 26 | let location = originalUrl 27 | 28 | // handles cases like example.com 29 | if (!location.startsWith('https://') && !location.startsWith('http://')) { 30 | location = `https://${location}` 31 | } 32 | 33 | ctx.status = 301 34 | ctx.redirect(location) 35 | ctx.body = 'Redirecting...' 36 | } 37 | 38 | /** 39 | * Main function. It handles the request by searching for an entry 40 | * in the database. If it exists, redirect, else, return not found. 41 | * @param {Object} ctx Koa's context 42 | */ 43 | const main = async ctx => { 44 | // example: https://chop.now.sh/abcdef => short := abcef 45 | const short = ctx.request.url.split('/').pop() 46 | // query db 47 | const document = await shortActions.getByShortPath(short) 48 | 49 | return document ? redirect(ctx, document.originalUrl) : notFound(ctx) 50 | } 51 | 52 | app.use(main) 53 | 54 | // to test locally, is there a better way? 55 | if (!process.env.NOW_REGION) app.listen(3000) 56 | module.exports = app.callback() 57 | -------------------------------------------------------------------------------- /back/helpers/short.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const Short = require('../models/Short.js') 4 | const uuid = require('./uuid.js') 5 | 6 | // https://zeit.co/docs/v2/deployments/environment-variables-and-secrets#from-the-cli 7 | const dbuser = process.env.mongo_chop_user 8 | const dbpassword = process.env.mongo_chop_pass 9 | 10 | // Remember to add your own secrets!!! 11 | if (!dbuser || !dbpassword) { 12 | throw new Error(`A user and password is required! Got user:${dbuser} and password:${dbpassword}`) 13 | } 14 | 15 | // connecting to mongo 16 | mongoose.connect(`mongodb://${dbuser}:${dbpassword}@ds115154.mlab.com:15154/chop`, { 17 | useNewUrlParser: true 18 | }) 19 | 20 | const getByShortPath = async (shortPath) => Short.findOne({ shortPath }).exec() 21 | 22 | const getByOriginalUrl = async (originalUrl) => Short.findOne({ originalUrl }).exec() 23 | 24 | const insert = async (originalUrl) => { 25 | let exists 26 | let shortPath 27 | // this loop should not run in most cases... 28 | // in case of collision, generate another uuid 29 | do { 30 | shortPath = uuid() 31 | exists = await getByShortPath(shortPath) 32 | } while (exists) 33 | 34 | const newDocument = new Short({ shortPath, originalUrl }) 35 | newDocument.save() 36 | 37 | return { originalUrl, shortPath } 38 | } 39 | 40 | module.exports = { 41 | getByShortPath, 42 | getByOriginalUrl, 43 | insert 44 | } 45 | -------------------------------------------------------------------------------- /back/helpers/uuid.js: -------------------------------------------------------------------------------- 1 | const uuidv4 = require('uuid/v4') 2 | 3 | // we can change this in the future for a hash function, if needed. 4 | module.exports = () => uuidv4().split('-').shift() 5 | -------------------------------------------------------------------------------- /back/middlewares/acceptedContentType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This middleware ensures that we are receiving a proper body. 3 | */ 4 | module.exports = async (ctx, next) => { 5 | if (ctx.is('application/json')) { 6 | await next() 7 | } else { 8 | ctx.status = 406 9 | ctx.accepts('application/json') 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /back/middlewares/errorHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This middleware is a central error handler for our app. 3 | */ 4 | const use = async (ctx, next) => { 5 | try { 6 | await next() 7 | } catch (err) { 8 | ctx.status = err.status || 500 9 | ctx.body = err.message 10 | ctx.app.emit('Error', err, ctx) 11 | } 12 | } 13 | 14 | const on = (err, ctx) => { 15 | console.error(err) 16 | } 17 | 18 | module.exports = { 19 | use, 20 | on 21 | } 22 | -------------------------------------------------------------------------------- /back/middlewares/responseTime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This middleware benchmarks the response time of the server. 3 | */ 4 | module.exports = async (ctx, next) => { 5 | const start = Date.now() 6 | await next() 7 | const ms = Date.now() - start 8 | ctx.set('X-Response-Time', `${ms}ms`) 9 | } 10 | -------------------------------------------------------------------------------- /back/models/Short.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const Short = mongoose.model('Short', { 4 | originalUrl: { 5 | type: String 6 | }, 7 | shortPath: { 8 | type: String, 9 | unique: true, 10 | index: true 11 | } 12 | }) 13 | 14 | module.exports = Short 15 | -------------------------------------------------------------------------------- /front/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |