[Yandex Cloud documentation](../../../index.md) > [Tutorials](../../index.md) > [Serverless technologies](../index.md) > Serverless-based backend > [URL shortener](index.md) > Management console

# Link shortener from the management console


To create a [link shortener](index.md) using the Yandex Cloud management console:

1. [Get your cloud ready](#before-begin).
1. [Set up hosting for the URL shortener page](#object-storage).
1. [Create a service account](#service-account).
1. [Create a database in Yandex Managed Service for YDB](#ydb).
1. [Set up a function in Yandex Cloud Functions](#function).
1. [Publish your URL shortener via Yandex API Gateway](#api-gw).
1. [Test the URL shortener](#test).

If you no longer need the resources you created, [delete them](#clear-out).

## Get your cloud ready {#before-begin}

Sign up for Yandex Cloud and create a [billing account](../../../billing/concepts/billing-account.md):
1. Navigate to the [management console](https://console.yandex.cloud) and log in to Yandex Cloud or create a new account.
1. On the **[Yandex Cloud Billing](https://center.yandex.cloud/billing/accounts)** page, make sure you have a billing account linked and it has the `ACTIVE` or `TRIAL_ACTIVE` [status](../../../billing/concepts/billing-account-statuses.md). If you do not have a billing account, [create one](../../../billing/quickstart/index.md) and [link](../../../billing/operations/pin-cloud.md) a cloud to it.

If you have an active billing account, you can create or select a [folder](../../../resource-manager/concepts/resources-hierarchy.md#folder) for your infrastructure on the [cloud page](https://console.yandex.cloud/cloud).

[Learn more about clouds and folders here](../../../resource-manager/concepts/resources-hierarchy.md).

### Required paid resources {#paid-resources}

The infrastructure support cost for the URL shortener includes:
* Fee for data storage (see [Yandex Object Storage pricing](../../../storage/pricing.md)).
* Fee for [YDB database](../../../ydb/concepts/resources.md#database) operations and data storage (see [Managed Service for YDB pricing](../../../ydb/pricing/serverless.md)).
* Fee for the number of [function](../../../functions/concepts/function.md) calls, computing resources allocated for the function, and outgoing traffic (see [Cloud Functions pricing](../../../functions/pricing.md)).
* Fee for the number of requests to the [API gateway](../../../api-gateway/concepts/index.md) and outgoing traffic (see [API Gateway pricing](../../../api-gateway/pricing.md)).

## Set up hosting for the URL shortener page {#object-storage}

To create a bucket, upload your URL shortener's HTML page to it, and configure it for static website hosting:

{% list tabs group=instructions %}

- Management console {#console}

  1. In the [management console](https://console.yandex.cloud), select your working folder.
  1. Select **Object Storage**.
  1. Click **Create bucket**.
  1. On the bucket creation page:
     1. Enter a name for the bucket.

        {% note warning %}

        Bucket names are unique throughout Object Storage, which means you cannot create two buckets with the same name, even in different folders belonging to different clouds.

        {% endnote %}

     1. Set the maximum size to `1 GB`.
     1. Set object read access to `For all`.
     1. Click **Create bucket** to complete the operation.
  1. Copy the HTML code and paste it into `index.html`:

     {% cut "HTML code" %}

     ```html
     <!DOCTYPE html>
     <html lang="en">
     
     <head>
       <meta charset="UTF-8">
       <title>URL shortener</title>
       <!-- warns against sending unnecessary GET requests to /favicon.ico -->
       <link rel="icon" href="data:;base64,iVBORw0KGgo=">
     </head>
     
     <body>
       <h1>Welcome</h1>
       <form action="javascript:shorten()">
         <label for="url">Enter a link:</label><br>
         <input id="url" name="url" type="text"><br>
         <input type="submit" value="Shorten">
       </form>
       <p id="shortened"></p>
     </body>
     
     <script>
       function shorten() {
         const link = document.getElementById("url").value
         fetch("/shorten", {
           method: 'POST',
           headers: {
             'Content-Type': 'application/json'
           },
           body: link
         })
         .then(response => response.json())
         .then(data => {
           const url = data.url
           document.getElementById("shortened").innerHTML = `<a href=${url}>${url}</a>`
         })
         .catch(error => {
           document.getElementById("shortened").innerHTML = `<p>${error} occurred. Try again.</p>`
         })
       }
     </script>
     
     </html>
     ```

     {% endcut %}

  1. Click the name of the bucket you created.
  1. Click **Upload objects**.
  1. Specify the `index.html` file you set up earlier.
  1. Click **Upload**.
  1. In the left-hand panel, select the **Website** tab.
  1. Select **Hosting**.
  1. Specify the website's home page: `index.html`.
  1. Click **Save**.

{% endlist %}

## Create a service account {#service-account}

To create a service account to enable interaction between the shortener components:

{% list tabs group=instructions %}

- Management console {#console}

  1. In the [management console](https://console.yandex.cloud), select the relevant folder.
  1. Navigate to **Identity and Access Management**.
  1. Click **Create service account**.
  1. Name the [service account](../../../iam/concepts/users/service-accounts.md): `serverless-shortener`.
  1. Click ![image](../../../_assets/console-icons/plus.svg) **Add role** and select `editor`.
  1. Click **Create**.
  1. Click the name of the service account you created.

     Save the service account ID, as you will need it later.

{% endlist %}

## Create a database in Managed Service for YDB {#ydb}

To create a database in Managed Service for YDB and configure it to store URLs:

{% list tabs group=instructions %}

- Management console {#console}

  1. Navigate to your working folder.
  1. In the list of services, select **Managed Service for&nbsp;YDB**.
  1. Click **Create a database**.
  1. Enter the database name: `for-serverless-shortener`.
  1. Select **Serverless** as the database type.
  1. Click **Create a database**.
  1. Wait for the database to start.

     While being created, your database will have the `Provisioning` status. Once it is ready for use, its status will change to `Running`.
  1. Click the database name.

     Save the **Endpoint** field value, as you will need it later.
  1. In the left-hand panel, select the **Navigation** tab.
  1. Select **Create** → **Table** on the right side of the page.
  1. Configure the table:
     * **Name** tables: `links`.
     * **Table type**: `Row table`.
  1. Add columns:
     * Column name: `id`; data type: `Utf8`. Set **Primary key**.
     * Column name: `link`; data type: `Utf8`.
  1. Click **Create table**.

{% endlist %}

## Set up a function in Cloud Functions {#function}

To create and set up a URL shortening function:

{% list tabs group=instructions %}

- Management console {#console}

  1. Navigate to your working folder.
  1. In the list of services, select **Cloud Functions**.
  1. Click **Create function**.
  1. Enter the name: `for-serverless-shortener`.
  1. Click **Create**.
  1. From the **Python** drop-down list, select the `python312` runtime.
  1. Click **Continue**.
  1. Copy the function code and paste it into `index.py` under **Function code**.

     {% cut "Function code" %}

     ```py
     import ydb
     import urllib.parse
     import hashlib
     import base64
     import json
     import os
     import traceback
     
     def decode(event, body):
         is_base64_encoded = event.get('isBase64Encoded')
         if is_base64_encoded:
             body = str(base64.b64decode(body), 'utf-8')
         return body
     
     def response(statusCode, headers, isBase64Encoded, body):
         # Always returning string to body
         if not isinstance(body, str):
             body = json.dumps(body, ensure_ascii=False)
         return {
             'statusCode': statusCode,
             'headers': headers,
             'isBase64Encoded': isBase64Encoded,
             'body': body,
         }
     
     def get_config():
         endpoint = os.getenv("endpoint")
         database = os.getenv("database")
         if endpoint is None or database is None:
             raise AssertionError("You need to specify both environment variables: endpoint and database")
         credentials = ydb.iam.MetadataUrlCredentials()
         return ydb.DriverConfig(endpoint, database, credentials=credentials)
     
     def execute(config, query, params):
         with ydb.Driver(config) as driver:
             try:
                 driver.wait(timeout=5, fail_fast=True)
             except Exception as e:
                 print("Connect failed to YDB:", e)
                 print(driver.discovery_debug_details())
                 raise
     
             session = driver.table_client.session().create()
             prepared_query = session.prepare(query)
             return session.transaction(ydb.SerializableReadWrite()).execute(
                 prepared_query,
                 params,
                 commit_tx=True
             )
     
     def insert_link(id, link):
         config = get_config()
         query = """
         DECLARE $id AS Utf8;
         DECLARE $link AS Utf8;
     
         UPSERT INTO links (id, link) VALUES ($id, $link);
         """
         params = {'$id': id, '$link': link}
         execute(config, query, params)
     
     def find_link(id):
         config = get_config()
         query = """
         DECLARE $id AS Utf8;
     
         SELECT link FROM links where id=$id;
         """
         params = {'$id': id}
         result_set = execute(config, query, params)
     
         if not result_set or not result_set[0].rows:
             return None
     
         # Paying attention to the structure of the result from ydb
         return result_set[0].rows[0].link
     
     def shorten(event):
         try:
             body = event.get('body')
             if body is None:
                 return response(400, {'Content-Type': 'application/json'}, False,
                                 {'error': 'No body in the request body'})
     
             body = decode(event, body)
     
             # Attempting to parse JSON with a url key; otherwise, treating body as a plain string
             url_value = None
             try:
                 parsed = json.loads(body)
                 if isinstance(parsed, dict):
                     url_value = parsed.get('url')
                 else:
                     # If not a JSON object is sent, ignoring
                     url_value = None
             except Exception:
                 # If body is not JSON, treating it as plain URL
                 url_value = body
     
             if not url_value:
                 return response(400, {'Content-Type': 'application/json'}, False,
                                 {'error': 'The url parameter was expected in the request body'})
     
             # Cleaning the URL from eventual coded characters
             clean_url = urllib.parse.unquote(url_value).strip()
             if not clean_url:
                 return response(400, {'Content-Type': 'application/json'}, False,
                                 {'error': 'Empty url'})
     
             link_id = hashlib.sha256(clean_url.encode('utf8')).hexdigest()[:6]
             insert_link(link_id, clean_url)
     
             # Returning the relative path; the frontend will add the origin by themselves
             return response(200, {'Content-Type': 'application/json'}, False,
                             {'url': f'/r/{link_id}'})
         except Exception as e:
             print("Exception in shorten():", e)
             traceback.print_exc()
             return response(500, {'Content-Type': 'application/json'}, False,
                             {'error': 'internal server error'})
     
     def redirect(event):
         try:
             # Securely retrieving path params
             path_params = event.get('pathParams') or event.get('pathParameters') or {}
             link_id = path_params.get('id')
             if not link_id:
                 # Probably, the full path was received in event['url'] or ['path']
                 url = event.get('url') or event.get('path') or ''
                 if url and url.startswith('/r/'):
                     link_id = url.split('/r/')[-1].split('?')[0]
     
             if not link_id:
                 return response(400, {'Content-Type': 'application/json'}, False, {'error': 'No ID'})
     
             redirect_to = find_link(link_id)
     
             if redirect_to:
                 return response(302, {'Location': redirect_to}, False, '')
             return response(404, {'Content-Type': 'application/json'}, False, {'error': 'The link does not exist'})
         except Exception as e:
             print("Exception in redirect():", e)
             traceback.print_exc()
             return response(500, {'Content-Type': 'application/json'}, False, {'error': 'internal server error'})
     
     def get_result(url, event):
         if url == "/shorten" or url.startswith("/shorten"):
             return shorten(event)
         if url.startswith("/r/"):
             return redirect(event)
         return response(404, {'Content-Type': 'application/json'}, False, {'error': 'The path does not exist'})
     
     def handler(event, context):
         url = event.get('url') or event.get('path') or ''
         if url:
             # Sometimes, the URL from the gateway comes with a question mark at the end
             if url.endswith('?'):
                 url = url[:-1]
             return get_result(url, event)
         return response(404, {'Content-Type': 'application/json'}, False, {'error': 'The function must be called via API Gateway.'})
     ```

     {% endcut %}

  1. Under **Function code**, create a file named `requirements.txt` and paste the following text into it:

     ```text
     ydb
     ```

  1. Specify the entry point: `index.handler`.
  1. Set the timeout to `5`.
  1. Select the `serverless-shortener` service account.
  1. Add these environment variables:
     * `endpoint`: Enter the first part of the previously saved **Endpoint** field value (preceding `/?database=`), e.g., `grpcs://ydb.serverless.yandexcloud.net:2135`.
     * `database`: Enter the second part of the previously saved **Endpoint** field value (following `/?database=`), e.g., `/ru-central1/r1gra875baom********/g5n22e7ejfr1********`.
  1. Click **Save changes**.
  1. Under **Overview**, enable **Public function**.

  Save the function ID, as you will need it later.

{% endlist %}

## Publish your URL shortener via API Gateway {#api-gw}

To publish your URL shortener via API Gateway:

{% list tabs group=instructions %}

- Management console {#console}

  1. Navigate to your working folder.
  1. From the list of services, select **API Gateway**.
  1. Click **Create API gateway**.
  1. In the **Name** field, enter `for-serverless-shortener`.
  1. Copy and paste the following code under **Specification**:

     {% cut "API gateway specification" %}

     ```yaml
     openapi: 3.0.0
     info:
       title: for-serverless-shortener
       version: 1.0.0
     paths:
       /:
         get:
           x-yc-apigateway-integration:
             type: object_storage
             bucket: <bucket_name>#<-- bucket name
             object: index.html # <-- HTML file name
             presigned_redirect: false
             service_account: <service_account_id> # <-- service_account_ID
           operationId: static
       /shorten:
         post:
           x-yc-apigateway-integration:
             type: cloud_functions
             function_id: <function_id> # <-- function ID
           operationId: shorten
       /r/{id}:
         get:
           x-yc-apigateway-integration:
             type: cloud_functions
             function_id: <function_id> # <-- function ID
           operationId: redirect
           parameters:
           - description: id of the url
             explode: false
             in: path
             name: id
             required: true
             schema:
               type: string
             style: simple
     ```

     {% endcut %}

     Edit the specification code as follows:
     * Replace `<service_account_id>` with the ID of the service account you created.
     * Replace `<function_id>` with the ID of the function you created.
     * Replace `<bucket_name>` with the name of the bucket you created.
  1. Click **Create**.
  1. Click the name of the API gateway you created.
  1. Copy the `url` value from the specification.

     Use this URL to [test the URL shortener](#test).

{% endlist %}

## Test the URL shortener {#test}

To make sure your shortener components interact properly:
1. In your browser, open the previously copied shortener URL.
1. In the input field, enter the URL you want to shorten.
1. Click **Shorten**.

   You will see the shortened URL below.
1. Open the shortened URL. It should lead to the same page as the original link.

## How to delete the resources you created {#clear-out}

To stop paying for the resources you created:
1. [Delete](../../../api-gateway/operations/api-gw-delete.md) `for-serverless-shortener`.
1. [Delete](../../../functions/operations/function/function-delete.md) the function in `for-serverless-shortener`.
1. [Delete](../../../ydb/operations/manage-databases.md#delete-db) the `for-serverless-shortener` YDB database.
1. [Delete](../../../storage/operations/buckets/delete.md) the bucket.

#### Useful links {#see-also}

* [Creating a URL shortener with Terraform](terraform.md)