Photo by Samantha Gades on Unsplash
Build Serverless url shortener with url expiration
We will build a url shortener application using AWS lambda, dynamoDB, api gateway, nodeJS and Serverless framework with url expiration.
A URL shortener is a tool that takes a long URL and converts it into a shorter, more manageable version. These shortened URLs can be easily shared and are typically used to save space in social media posts, text messages, and other places where space is limited. In this article, we will go over how to build a serverless URL shortener using AWS DynamoDB, Lambda, API gateway and Node.js. We will use serverless framework.
The Serverless Framework is a popular open-source framework for building and deploying serverless applications on various cloud providers, including AWS. It can be used to simplify the process of creating, configuring, and deploying the various services needed to build a serverless URL shortener.
To build a URL shortener using the Serverless Framework, you'll need to create a new project and add the necessary configuration for the different services you'll be using. Here's an example of how you can set up a serverless URL shortener with URL expiration using the Serverless Framework, DynamoDB, Lambda, and Node.js:
Create a new project using the Serverless Framework.
$ serverless create --template aws-nodejs --path my-url-shortener
Create resources in Serverless Framework
service: myNewUrlService
provider:
name: aws
runtime: nodejs12.x
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:*
Resource: "*"
package:
patterns:
- '!package.json'
- '!package-lock.json'
- '!node_modules/**'
- '!README.md'
functions:
hello:
handler: handler.hello # required, handler set in AWS Lambda
name: shortUrl-sls
description: Description of what the lambda function does.
layers:
- arn:aws:lambda:us-east-1:<res_no>:layer:expresss-nanoid-validurl-module:1
events:
- http:
path: api/
method: post
- http:
path: api/{id}
method: get
request:
parameters:
paths:
id: true
resources:
Resources:
usersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: shortUrlTable-sls
AttributeDefinitions:
- AttributeName: uniqueId
AttributeType: S
KeySchema:
- AttributeName: uniqueId
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true
Add logic in the handler file.
const AWS = require("aws-sdk");
const validUrl = require("valid-url");
const { customAlphabet } = require("nanoid");
const dynamoDB = new AWS.DynamoDB.DocumentClient();
const nanoid = customAlphabet(
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVXYZ",
5
);
const idLength = 4;
const redirectToWrongUrl =
"https://www.meme-arsenal.com/memes/c9e6371faa3b57eaee1d35595ca8e910.jpg";
const sendResponse = (code, bool, msg, link) => {
return {
statusCode: code,
body: JSON.stringify({
success: bool,
message: msg,
items: link,
}),
};
};
const createUrl = async (uniqueId, longUrl, ttl) => {
await dynamoDB
.put({
Item: { uniqueId, longUrl, ttl },
TableName: "shortUrlTable-sls",
})
.promise();
const params = {
uniqueId,
longUrl,
shortUrl: `https://<api-gateway-id>.execute-api.us-east-1.amazonaws.com/dev/api/${uniqueId}`,
};
return sendResponse(201, true, "Url created successfully", params);
};
const getLongUrl = async (uniqueId) => {
const res = await dynamoDB
.get({
TableName: "shortUrlTable-sls",
Key: { uniqueId },
})
.promise();
console.log("curr time :: ", Math.round(Date.now() / 1000));
if (Object.keys(res).length === 0) {
console.log("url do not exist");
return redirectToWrongUrl;
}
console.log("Item time :: ", res.Item.ttl);
if (res.Item.ttl <= (Date.now() / 1000)) {
console.log("url expired");
return redirectToWrongUrl;
}
return res.Item.longUrl;
};
const checkAvailability = async (longUrl) => {
const res = await dynamoDB
.scan({
TableName: "shortUrlTable-sls",
})
.promise();
console.log(res.Items.length);
for (let i = 0; i < res.Items.length; i++) {
if (res.Items[i].longUrl === longUrl) {
const params = {
uniqueId: res.Items[i].uniqueId,
longUrl: res.Items[i].longUrl,
shortUrl: `https://<api-gateway-id>.execute-api.us-east-1.amazonaws.com/dev/api/${res.Items[i].uniqueId}`,
};
return params;
}
}
return false;
};
module.exports.hello = async (event) => {
try {
if (event.httpMethod === "POST") {
const { longUrl } = JSON.parse(event.body);
if (!validUrl.isUri(longUrl)) { // If not a valid longUrl
return sendResponse(404, false, "invalid url", []);
}
const bools = await checkAvailability(longUrl);
if (bools != false) { // redundancy
return sendResponse(201, true, "Url already available", bools);
}
const uniqueId = nanoid(idLength);
// generating epoch time
const ttl = (Math.round(Date.now() / 1000)) + 120; //60*2 mins
console.log(ttl);
return createUrl(uniqueId, longUrl, ttl);
}
if (event.httpMethod === 'GET') {
let { id } = event.pathParameters;
const longUrl = await getLongUrl(id);
const response = {
statusCode: 301,
headers: {
Location: longUrl,
}
};
return response;
}
} catch (error) {
console.log(error);
}
};
Refer to this repository for more information.