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.