Minimal Url Shortener with .NET Minimal Api, Htmx, and DaisyUI
Table of Contents
This is a fun and lightweight project that combines the power of .net minimal api, htmx, and daisyUI to create a fun web application for shortening and managing URLs with just a few lines of code. It’s uses, LiteDB, Hashids, and leverages the minimal api capabilities for an optimal experience. The addition of htmx ensures lightweight and fast frontend interactions, while daisyUI provides additional styling with Tailwind CSS.
Goals #
As a user, I had some ideas of what I want from this project. I’m playing an old school word game that I can send a link to my opponent but every message has a char limitation. Since I’m using a third party apps I can use easy, lightweight app that I can manage.
As a developer, I wanted to play with htmx and .net minimal api and see what we can do together. Since this was going to be an app for personal use , and I had no intention of turning it into anything else. So the main goal is showing how handy htmx and minimal api together.
Features #
- URL Shortening: Users can enter a long URL and receive a shortened version that redirects to the original URL.
- URL Management: Users can view a list of all their shortened URLs and easily copy them for sharing.
- Lightweight and Fast: The combination of htmx and DaisyUI ensures fast frontend interactions and smooth user experience.
- Data Persistence: LiteDB is used as the database to store the shortened URLs, ensuring data persistence across sessions.
Todos 🚀 #
- Serves our static files (cool html page pampered by htmx and css)
- Magic of dotnet minimal api’s http api endpoints, where the frontend can call via htmx (fancy ajax requests)
- The backend api saves the given urls and bringing back HTML via
RazorComponentResult
.RazorComponentResult
is recently introduced with .net 8 check this out
When I pair with RazorComponentResult with the minimal api, It’s become very powerful tool for htmx. I loved it!
User Interface #
Although this was going to be a personal tool, and I wanted it to work on a local-first setup, It could be nice to have responsive and easy layout with tailwindcss, in addition easy components with daisyUI.
It’s so easy to use and I choose the simplest way just adding these two lines of in the <head>
tag.
I follow
daisyui cdn approach and I aware of the warning for not recommended for production but for fun yes ☕
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
I’m not gonna dive into too much detail with the design the source code is on github. But I end-up a page like this which all I need is that copy button 🤟 :
Development #
Htmx makes it so simple and minimal I like it. index.html
<form class="grid grid-cols-7 m-4 gap-2"
hx-post="/shorten"
hx-target="#urls"
hx-swap="innerHTML"
hx-indicator="#ind">
<input class="col-span-5 input input-bordered"
type="url"
name="longUrl"
id="longUrl"
value=""
placeholder="Enter your long url here"
/>
<button class="btn btn-accent col-span-2">
<span class="material-icons text-md">link</span>
Short it!
</button>
</form>
<div class="container max-w-xl mx-auto">
<progress id="ind" class="htmx-indicator progress progress-primary w-full"></progress>
</div>
<div id="urls" class="flex my-8 gap-8" hx-get="/history?p=" hx-trigger="load" hx-swap="innerHTML">
</div>
Let’s have a look at Program.cs
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
WebRootPath = "Frontend"
});
builder.Services.AddRazorComponents();
builder.Services.AddCors
(
cors => cors
.AddPolicy(name: "default-policy", policy => policy
.AllowAnyHeader()
.AllowAnyOrigin()
.AllowAnyMethod())
);
builder.Services.AddSingleton<ILiteDatabase, LiteDatabase>(_ => new LiteDatabase("minimal-url-shortener.db"));
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
app.AddHtmxEndpoints();
var fileOptions = new DefaultFilesOptions();
fileOptions.DefaultFileNames.Clear();
fileOptions.DefaultFileNames.Add("index.html");
fileOptions.DefaultFileNames.Add("styles.css");
fileOptions.DefaultFileNames.Add("favicon.ico");
app.UseDefaultFiles(fileOptions);
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "Frontend"))
});
app.UseCors("default-policy");
app.Run();
- It creates application
builder
sets up a web application with an options asWebRootPath
is “Frontend” folder. - Configures services like
LiteDB
,CORS
, andRazor Components
- Defines custom endpoints for htmx which I will mention details below, and configures static file serving for a “Frontend” folder.
The Program.cs
is ready I need to handle requests. Here is the Endpoints.cs
public static class Endpoints
{
public static void AddHtmxEndpoints(this WebApplication app)
{
Hashids _hashIds = new Hashids("SuperSecretSaltKey", 6);
app.MapGet("/history", (HttpContext context, [FromServices] ILiteDatabase _context) =>
{
var p = context.Request.Query["p"].ToString();
var baseUrl = UrlExtensions.GetAppUrl(context.Request);
int pageSize = 10, page = 1;
if (!string.IsNullOrEmpty(p))
int.TryParse(p, out page);
var db = _context.GetCollection<UrlModel>();
var entries = db.Query().OrderByDescending(urlModel => urlModel.Id);
var model = PagedList<UrlModel>.Create(entries,page,pageSize);
model.Items.ForEach(l=>l.ShortUrl = baseUrl+l.ShortUrl);
return RazorExtensions.Component<UrlModelList>(model);
});
app.MapPost("/shorten", async (HttpContext context, [FromServices] ILiteDatabase _context) =>
{
var form = await context.Request.ReadFormAsync();
var longUrl = form.ContainsKey("longurl") ? form["longurl"].ToString() : string.Empty;
var baseUrl = UrlExtensions.GetAppUrl(context.Request);
if (string.IsNullOrEmpty(longUrl))
return RazorExtensions.Component<Empty>();
var db = _context.GetCollection<UrlModel>(BsonAutoId.Int32);
var model = new UrlModel();
model.LongUrl = longUrl;
model.CreatedAt = DateTime.Now;
var id = db.Insert(model);
model.ShortUrl = _hashIds.Encode(id);
db.Update(model);
model.ShortUrl= baseUrl+model.ShortUrl;
return RazorExtensions.Component<UrlModelDetail>(model);
});
app.MapGet("/{shortUrl}", (string shortUrl, [FromServices] ILiteDatabase _context) =>
{
var id = _hashIds.Decode(shortUrl);
var tempId = id[0];
var db = _context.GetCollection<UrlModel>();
var entry = db.Query().Where(x => x.Id.Equals(tempId)).ToList().FirstOrDefault();
if (entry != null) return Results.Redirect(entry.LongUrl);
return RazorExtensions.Component<Empty>();
});
}
}
MapGet("/history", ...)
endpoint handles requests to display a paginated history of shortened URLs that it displays on index page. It retrieves URL entries from a LiteDB collection, orders them by creation date, paginates the results, and formats them for display using Razor components.MapPost("/shorten", ...)
endpoint handles URL shortening requests. It reads a long URL from the request form, creates a new URL model, inserts it into a LiteDB collection, generates a short URL using Hashids, updates the model with the short URL, and returns the details using a Razor component.MapGet("/{shortUrl}", ...)
endpoint handles requests to redirect to the original URL based on a short URL. It decodes the short URL using Hashids, queries the LiteDB collection for the corresponding URL model, and redirects to the original URL if found; otherwise, it displays an empty page using a Razor component.
Almost there! I need to create two razor component that they will parse url list table together with paging nav html and single url shortener result html with htmx tags. as a demonstration they will like
UrlModelList.razor
<table class="w-full">
@foreach (var item in Model.Items)
{
<tr>
<td>
<a href="@item.ShortUrl">@item.ShortUrl</a> <br/>
<a href="@item.LongUrl" target="_blank">@item.LongUrl</a>
</td>
</tr>
}
</table>
@for (var i = 1; i <= Model.TotalPages; i++)
{
<li>
<a
hx-get="/history?p=@i"
hx-target="#urls"
hx-swap="innerHTML"
href="#"
hx-indicator="#ind">@i</a>
</li>
}
@code
{
[Parameter]
public PagedList<UrlModel> Model { get; set; } = new();
}
UrlModelDetail.razor
<div class="container">
<a href="@Model.ShortUrl">@Model.ShortUrl</a> <br/>
<a href="@Model.LongUrl" target="_blank">@Model.LongUrl</a>
</div>
@code
{
[Parameter]
public UrlModel Model { get; set; } = new();
}
I eventually reached a point where I save url’s instantly get response with in the same page very fast.
Conclusion #
It took me about 2 hours to put all together the small pieces but how lightweight and easy htmx and dotnet minimal api with the new Razor components all together. I enjoyed very much and wanted to share this repo with a blog post feel free to have look.
Minimal URL Shortener with .NET Minimal API, htmx, and DaisyUI