<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gábor Maksa</title>
    <description>The latest articles on DEV Community by Gábor Maksa (@anwitars).</description>
    <link>https://dev.to/anwitars</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3440460%2Ffc0defaa-ea1d-4dae-9c16-323a664caceb.png</url>
      <title>DEV Community: Gábor Maksa</title>
      <link>https://dev.to/anwitars</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anwitars"/>
    <language>en</language>
    <item>
      <title>Why did I build a transparent, account-free, open-source URL shortener?</title>
      <dc:creator>Gábor Maksa</dc:creator>
      <pubDate>Mon, 18 Aug 2025 18:40:05 +0000</pubDate>
      <link>https://dev.to/anwitars/why-did-i-build-a-transparent-account-free-open-source-url-shortener-45cm</link>
      <guid>https://dev.to/anwitars/why-did-i-build-a-transparent-account-free-open-source-url-shortener-45cm</guid>
      <description>&lt;p&gt;I've always been cautious about clicking shortened URLs. Sometimes they feel sketchy, and I want to know where they lead before committing. Sure, you could use curl or third-party "unshortener" websites, but that's &lt;strong&gt;clunky&lt;/strong&gt; — and it comes with a subtle problem: these tools actually &lt;strong&gt;register a visit&lt;/strong&gt; on the shortener itself. The person who created the link thinks someone clicked it, even though nobody really did.&lt;/p&gt;

&lt;p&gt;That little frustration got me thinking: what if the shortener &lt;em&gt;itself&lt;/em&gt; let you &lt;strong&gt;safely peek at the destination URL&lt;/strong&gt;, without counting it as a click?&lt;/p&gt;

&lt;p&gt;And that's how &lt;strong&gt;Trails&lt;/strong&gt; was born: a simple, &lt;strong&gt;privacy-first URL shortener that gives users transparency and control, while respecting their privacy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Before I dive into the story of how I built it, here are the core principles that guided its design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transparency&lt;/strong&gt; — Peek URLs safely; see creation date, expiry, and visit counts. Trails are immutable to maintain trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No accounts&lt;/strong&gt; — Each Trail has a unique token; owning the token lets you delete or manage it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy&lt;/strong&gt; — IPs are never stored raw — only hashed using SHA-256, so identities remain safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting the motivation
&lt;/h2&gt;

&lt;p&gt;Most of my earlier projects (that has never seen daylight) were built using either &lt;a href="https://www.starlette.io/" rel="noopener noreferrer"&gt;Starlette&lt;/a&gt; or &lt;a href="https://github.com/tokio-rs/axum" rel="noopener noreferrer"&gt;axum&lt;/a&gt;. While I gained valuable experience, &lt;strong&gt;none of them were truly complete or polished&lt;/strong&gt;. As an ambitious developer, this was a point of frustration, especially when I saw job descriptions on LinkedIn that consistently asked for hands-on projects.&lt;/p&gt;

&lt;p&gt;It was then that I noticed a common theme: many Python-related roles specifically requested experience with FastAPI. I had an idea: what if I took the project I had already started and rebuilt it using &lt;strong&gt;FastAPI&lt;/strong&gt; and &lt;strong&gt;Next.js&lt;/strong&gt; — two of the &lt;strong&gt;most in-demand frameworks&lt;/strong&gt; — while also focusing on &lt;strong&gt;high-quality documentation&lt;/strong&gt;? I started building again, this time with a clear goal in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laying down the foundations
&lt;/h2&gt;

&lt;p&gt;A URL shortener service is quite simple: take in a URL, generate a unique (preferably short) identifier, store both of them in a database, and whenever someone tries to access the short identifier, just redirect them to the long URL — that's it.&lt;/p&gt;

&lt;p&gt;I just needed a few things to clarify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Which database to use?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Which framework to use?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How to write readme, documentation and how do I even launch a project?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Where to host it?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Database
&lt;/h3&gt;

&lt;p&gt;Choosing the database was straightforward: &lt;a href="https://www.postgresql.org/" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt;. &lt;strong&gt;I have the most experience with it, very easy to spin up, has a lot of trust and is open-source&lt;/strong&gt; (I guess the last two come hand-in-hand).&lt;/p&gt;

&lt;p&gt;I've always found &lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt; to be a &lt;strong&gt;lifesaver&lt;/strong&gt; for quickly setting up the services an application needs. For Trails, I used a simple &lt;code&gt;docker-compose.yml&lt;/code&gt; file to spin up our PostgreSQL database. &lt;strong&gt;This keeps the setup process clean and consistent.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trails_db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trails&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5577:5432"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;image: postgres:latest&lt;/code&gt; — Pulls the official PostgreSQL image.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;container_name: trails_db&lt;/code&gt; — Names the container for easy reference.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;environment&lt;/code&gt; — Sets up the database user, password, and name.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ports&lt;/code&gt; — Maps the default PostgreSQL port (5432) inside the container to port 5577 on my local machine to avoid conflicts with other instances.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get the database up and running, just need a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then connect to it using the specified port and credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;-p&lt;/span&gt; 5577 &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-d&lt;/span&gt; trails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Backend
&lt;/h3&gt;

&lt;p&gt;My usual approach for a Python web API is to manually combine libraries like &lt;strong&gt;Starlette, Pydantic, and Uvicorn&lt;/strong&gt;. I realized &lt;strong&gt;FastAPI&lt;/strong&gt; was a &lt;strong&gt;powerful all-in-one solution&lt;/strong&gt; that bundled these tools, allowing me to &lt;strong&gt;focus on the core logic&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I just had to define my routes and their resolvers, and the application was ready to go. On top of that, &lt;strong&gt;FastAPI automatically generates comprehensive documentation&lt;/strong&gt; from the Pydantic models I use for data validation. With just a few descriptions added to the code, I had a &lt;strong&gt;fully documented API that implements the OpenAPI standard&lt;/strong&gt;, without any extra effort.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend
&lt;/h3&gt;

&lt;p&gt;My primary passion is backend development, but I wanted to build a &lt;strong&gt;user-friendly&lt;/strong&gt; frontend to complete this project. Given my experience with &lt;a href="https://react.dev/" rel="noopener noreferrer"&gt;React&lt;/a&gt;, &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; was the obvious choice due to its excellent documentation and ease of use.&lt;/p&gt;

&lt;h4&gt;
  
  
  Getting started
&lt;/h4&gt;

&lt;p&gt;The start was easy, I just needed to run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-next-app@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and go through the prompts.&lt;/p&gt;

&lt;p&gt;The routing was also straightforward, I just needed this file structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;src
├── app
│   ├── about
│   │   └── page.tsx
│   ├── contact
│   │   └── page.tsx
│   ├── error.tsx
│   ├── globals.css
│   ├── info
│   │   ├── page.tsx
│   │   └── &lt;span class="o"&gt;[&lt;/span&gt;trailid]
│   │       ├── DeleteButton.tsx
│   │       └── page.tsx
│   ├── layout.tsx
│   ├── page.tsx
│   ├── peek
│   │   ├── page.tsx
│   │   └── &lt;span class="o"&gt;[&lt;/span&gt;trailid]
│   │       └── page.tsx
│   └── saved
│       └── page.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pages are automatically generated by Next.js, with built-in dynamic routing.&lt;/p&gt;

&lt;h4&gt;
  
  
  The issue with environment variables
&lt;/h4&gt;

&lt;p&gt;There was only one issue that I still have not found the best solution for — &lt;strong&gt;environment variables&lt;/strong&gt;. I wanted the website to support both development and production environments without the need to rebuild the application.&lt;/p&gt;

&lt;p&gt;By default, you can use environment variables with Next.js, but they are only available &lt;strong&gt;at build time&lt;/strong&gt;. This means that the following code will not work as expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we look at this code and see &lt;code&gt;process.env.&lt;/code&gt; &lt;strong&gt;we could assume that the environment variable will be available at runtime, but it is not&lt;/strong&gt;. When we build the application, Next.js will replace &lt;code&gt;process.env.*&lt;/code&gt; with the actual value of that environment variable, making it &lt;strong&gt;static&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This caused some problems while deploying the application. A workaround I found is to &lt;strong&gt;create an API route that returns the environment variables I need&lt;/strong&gt; (as a &lt;em&gt;config&lt;/em&gt;), and then &lt;strong&gt;fetch them from the frontend&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Development and production environments can use different configurations without rebuilding the application.&lt;/li&gt;
&lt;li&gt;I can later extend the API to return more configuration options if needed.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Cons&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;It adds an extra API call to fetch the configuration (although it is cached, so it is not a big deal).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Seems like a good trade-off to me, so I went with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app/api/config/route.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;AppConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can fetch and cache it using a helper function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/config.ts&lt;/span&gt;
&lt;span class="c1"&gt;// some code before...&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getAppConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appConfig&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isServerSide&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_API_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NEXT_PUBLIC_API_URL is not defined in the environment variables.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;AppConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;appConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;AppConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to fetch app config:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Type safety to the max
&lt;/h4&gt;

&lt;p&gt;If you are curious about how I managed to write a &lt;em&gt;100% type-safe API client&lt;/em&gt; for the Next.js frontend leveraging Trails' OpenAPI spec, a detailed article is on the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation
&lt;/h2&gt;

&lt;p&gt;I believe that automation — &lt;em&gt;when implemented strategically&lt;/em&gt; — can be a &lt;strong&gt;game-changer&lt;/strong&gt;. For Trails, I designed a &lt;strong&gt;Continuous Integration (CI)&lt;/strong&gt; pipeline using &lt;strong&gt;GitHub Actions&lt;/strong&gt; to streamline the development process and ensure reliability.&lt;/p&gt;

&lt;p&gt;This workflow automates two key tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On every push, it &lt;strong&gt;runs all tests&lt;/strong&gt; to catch regressions early.&lt;/li&gt;
&lt;li&gt;On every new tag, it &lt;strong&gt;automates the release process&lt;/strong&gt; by creating a new GitHub release with notes from the &lt;code&gt;CHANGELOG.md&lt;/code&gt; and then &lt;strong&gt;building and pushing the latest Docker image&lt;/strong&gt; to our container registry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check out the full configuration &lt;a href="https://github.com/anwitars/tiny-trails/blob/master/.github/workflows/ci.yml" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Deployment
&lt;/h3&gt;

&lt;p&gt;After building &lt;strong&gt;Trails&lt;/strong&gt;, I knew it deserved a more robust and professional home than my personal Raspberry Pi. I started looking for a hosting service that could handle my requirements.&lt;/p&gt;

&lt;p&gt;There was not really much that I needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker container support&lt;/strong&gt; — as both of the backend and the frontend are containerized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-class PostgreSQL support&lt;/strong&gt; — so I can easily make backups and manage the database. This one is not a must, as PostgreSQL has docker images, but it is a nice-to-have.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then &lt;a href="https://railway.com/" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; caught my attention. &lt;strong&gt;It is a platform that has both of my requirements&lt;/strong&gt;, and has a &lt;a href="https://railway.com/pricing" rel="noopener noreferrer"&gt;free starting tier&lt;/a&gt;. I signed up, and started deploying the application.&lt;/p&gt;

&lt;p&gt;Deployment was a no-brainer. I created 3 services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; — I just had to select the option, and it already created a PostgreSQL instance for me, with a connection string that I could use in my application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt; — provided the Docker image url, and it automatically pulled the image and started the container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt; — same as the backend.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Domain
&lt;/h3&gt;

&lt;p&gt;In the past, I've used simpler domain registrars for my personal projects, &lt;strong&gt;but for Trails, I wanted something that offered a more robust set of features&lt;/strong&gt;. I chose &lt;a href="https://cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt; for its powerful DNS management and security benefits, which made it a &lt;strong&gt;perfect candidate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;While Railway's custom domain setup guide was holding my hand, I was able to add a CNAME record to my domain's DNS settings. This tells Cloudflare to route all traffic for &lt;code&gt;trls.link&lt;/code&gt; and &lt;code&gt;api.trls.link&lt;/code&gt; to their respective services running on Railway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Future of Trails
&lt;/h2&gt;

&lt;p&gt;Now that &lt;strong&gt;Trails has come to life&lt;/strong&gt;, I am focused on its next phase of development. &lt;strong&gt;The backend is solid and the core features are functional&lt;/strong&gt;, but still needs a lot of love, particularly on the frontend. My goal is to add more advanced features that build on the project's core principles of &lt;strong&gt;transparency and privacy&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  More Precise Metrics
&lt;/h3&gt;

&lt;p&gt;Currently, users can see the total and unique number of visits for each Trail. However, I want to provide more precise way to see this data: &lt;strong&gt;timeframes&lt;/strong&gt;. It is not a big issue to implement it, as I already store the timestamp for each visit, I just need to find a way to &lt;strong&gt;aggregate this data and return it in a user-friendly way&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trending Trails
&lt;/h3&gt;

&lt;p&gt;Trails is not a social media platform — so why would I want to show "trending" Trails? Well, &lt;em&gt;why not?&lt;/em&gt; &lt;strong&gt;People are curious, and it would be fun to see which Trails are currently popular.&lt;/strong&gt; I could implement a simple algorithm that calculates a score for each Trail based on the last 24 hours of unique visits, how well the unique visits are distributed over time, and some other things I find interesting. We will see how it goes, but I think &lt;strong&gt;it would be a weird yet fun feature to have that helps Trails stand out&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Trails
&lt;/h3&gt;

&lt;h4&gt;
  
  
  The Idea
&lt;/h4&gt;

&lt;p&gt;If the previous feature idea was not exciting enough, what about Trails with &lt;strong&gt;dynamic URL resolution&lt;/strong&gt;? Okay, this might sound like something that would completely destroy the main purpose of this service: being transparent — &lt;strong&gt;but hear me out&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Imagine a Trail that redirects to a different URL based on the time of day, current date or some header value.&lt;/strong&gt; For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*Click* --&amp;gt; [Is it before 20th August 2025?] -----YES-----&amp;gt; https://some-website.com/giveaway/
                          |
                          +-----------------------NO------&amp;gt; https://some-website.com/news/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simple example, but you can imagine more complex scenarios. Still, the question remains: &lt;em&gt;How would I make it transparent?&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Potential Solution
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Create a new endpoint that &lt;strong&gt;clearly shows the user that the Trail they are about to visit is dynamic&lt;/strong&gt;. The static Trails would remain &lt;code&gt;/t/{trailid}&lt;/code&gt;, while the dynamic ones would be &lt;code&gt;/td/{trailid}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Adjust the peek endpoint to return a &lt;strong&gt;graph of the possible destinations&lt;/strong&gt; (so that users can examine every possible outcome), as well as &lt;strong&gt;what conditions are met, and what the destination would be for the current request&lt;/strong&gt; — for a quick decision making.&lt;/li&gt;
&lt;li&gt;Conditions must be one of the few predefined ones, such as:

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time of day&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Current date&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some header value&lt;/strong&gt; (e.g. &lt;code&gt;User-Agent&lt;/code&gt;, &lt;code&gt;Referer&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;And maybe more&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Limit the number of conditions and/or destinations&lt;/strong&gt; to ensure the service remains performant while keeping the &lt;strong&gt;transparency intact&lt;/strong&gt; (or at least as much as possible).&lt;/li&gt;

&lt;/ul&gt;



&lt;p&gt;&lt;strong&gt;And there it is — Trails was born. Alive, breathing, and ready to test a new idea&lt;/strong&gt; in the world of URL shorteners. It all started as a way to prove my skills, but now I see it as a &lt;strong&gt;potential tool that can help people navigate the web more safely and transparently&lt;/strong&gt;. It is still in its early days, but I am excited to see where it goes.&lt;/p&gt;

&lt;p&gt;I invite you to explore &lt;a href="https://trls.link/" rel="noopener noreferrer"&gt;Trails&lt;/a&gt; for yourself or dive into the full source code on &lt;a href="https://github.com/anwitars/tiny-trails" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. If you are curious about the deeper technical details regarding the API client of the Next.js frontend, stay tuned for an upcoming article that will cover &lt;em&gt;how I achieved 100% type safety using Trails' OpenAPI spec&lt;/em&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>programming</category>
      <category>webdev</category>
      <category>api</category>
    </item>
  </channel>
</rss>
