Build a URL Shortener API

Build a REST API in C# that shortens URLs and redirects visitors.

Introduction

30 Second Summary

Every time you click a short link on social media or in an email, a server somewhere receives that request, looks up the original destination, and sends your browser there in milliseconds. That redirect is one of the most common patterns on the web.

In this project, you will build your own URL shortener API using ASP.NET Core and C#. You will accept long URLs, generate short codes, store them in a database, and redirect visitors when they follow a short link.

What You'll Build

You will paste a long URL into your running API, get back a short code like http://localhost:5000/abc123, and watch your browser redirect to the original destination when you visit that link.

By the end of this project, you'll have:

  • A working REST API that accepts a long URL via a POST request and returns a unique short code you can share.
  • A real SQLite database managed by Entity Framework Core that persists your shortened links across application restarts.
  • A functioning HTTP redirect endpoint that proves your API handles real network traffic end-to-end. Paste the short link in any browser and land on the original page.
  • Secret Mission: Add click tracking that counts how many times each short link has been visited.

Are there any prerequisites?

You should be comfortable with basic C# concepts like classes, control flow, and object-oriented programming. No prior experience with APIs or databases is needed.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters for your development skills.

Create the Project and Install Dependencies

To build a URL shortener, you need a running web server that can listen for requests and send responses. Without that foundation, there is nothing to build on top of.

In this step you will scaffold a new ASP.NET Core project, install Entity Framework Core packages for your database, and confirm you have a live HTTP endpoint you can hit in your browser.

In this step, get ready to:
  • Scaffold a new .NET 10 web API project and confirm it serves a response.
  • Install Entity Framework Core and the dotnet-ef CLI tool.
  • Replace the template code with a clean Hello World endpoint.
Scaffold the project and confirm it runs
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Scaffold your new web API project by running this command:
dotnet new webapi -o UrlShortener

You should see output confirming that the template was created successfully.

What did this command create?

The dotnet new webapi command generated a minimal API project inside a new UrlShortener folder. It includes Program.cs (your app's entry point), a Properties/launchSettings.json file (server config), and a .csproj file (project metadata).

The template also adds a sample WeatherForecast endpoint you will use to verify the server runs before replacing it later.

  • Move into the project folder and open it in VS Code by running these commands:
cd UrlShortener
code -r .

VS Code reloads with the UrlShortener folder open. If a dialog asks whether you trust the authors of the files, select Yes, I trust the authors.

code command not found on macOS?

On macOS, the code command must be installed into your PATH first. Open VS Code manually, press Cmd+Shift+P to open the Command Palette, type Shell Command: Install 'code' command in PATH, and select it. Restart your terminal, then run code -r . again.

Help me fix the code command on macOS

  • Open the integrated terminal in the new VS Code window by pressing Ctrl+`.
  • Now configure the server port. In the Explorer sidebar on the left, expand the Properties folder and click launchSettings.json to open it.
  • Select all the content in this file and replace it with the following:
{
  "profiles": {
    "http": {
      "applicationUrl": "http://localhost:5000"
    }
  }
}

What does this file do?

The launchSettings.json file tells the .NET dev server which URL and port to listen on. By setting applicationUrl to http://localhost:5000, you give yourself a predictable address to visit throughout this project.

  • Save launchSettings.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Start the server by running this command in the terminal:
dotnet run
  • Open your browser and visit http://localhost:5000/weatherforecast.

You should see a JSON array of random weather data in the browser. This confirms the template's sample endpoint is working.

  • Back in your terminal, press Ctrl+C to stop the server.

Not seeing weather data in the browser?

Make sure you saved launchSettings.json before running dotnet run. Also confirm the URL is exactly http://localhost:5000/weatherforecast with no trailing slash or typo.

If the terminal shows a port conflict error, another process is using port 5000. Stop that process or change the port in launchSettings.json to another number like 5001.

Help me debug the localhost connection

Install EF Core packages and the dotnet-ef tool

Your URL shortener needs a database to remember which short codes map to which URLs. Entity Framework Core is the official .NET library for talking to databases. You will install it now so it is ready for the next step.

  • Install the SQLite database provider by running this command:
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 10.0.10
  • Install the design-time tools package by running this command:
dotnet add package Microsoft.EntityFrameworkCore.Design --version 10.0.10

Why two packages?

The Sqlite package lets your app read and write to a SQLite database file. The Design package enables the migration tooling that creates and updates your database schema from C# code.

  • Install the dotnet-ef global CLI tool by running this command:
dotnet tool install --global dotnet-ef --version 10.0.10
  • Confirm the tool installed successfully by running:
dotnet ef --version

You should see 10.0.10 printed in the terminal.

Seeing 'tool already installed' error?

If you previously installed dotnet-ef at a different version, the install command fails. Run the following command to update it instead:

dotnet tool update --global dotnet-ef --version 10.0.10

Then re-run dotnet ef --version to confirm it now shows 10.0.10.

Help me update dotnet-ef to the right version

Clean up the template and set up Program.cs for a minimal API

The template's weather forecast code was useful for proving the server works. Now you will replace it with a clean starting point for your own API.

  • In the Explorer sidebar, click Program.cs to open it.
  • Select all the code in the file (Cmd+A on macOS or Ctrl+A on Windows) and replace it with the following:
// Import EF Core (used in later steps)
using Microsoft.EntityFrameworkCore;
using UrlShortener.Data;
using UrlShortener.Models;

// Create the web application builder
var builder = WebApplication.CreateBuilder(args);

// Build the application
var app = builder.Build();

// Map a simple GET endpoint at the root URL
app.MapGet("/", () => "Hello World!");

// Start the server
app.Run();

What does this code do?

  • WebApplication.CreateBuilder(args) sets up the web server with default configuration.
  • app.MapGet("/", ...) registers a handler that responds with plain text when someone visits the root URL.
  • app.Run() starts the server and keeps it listening for incoming requests.

This is a Minimal API. Everything lives in a single file with no extra classes or folders needed.

  • Save Program.cs.

✔️ Awesome, I've got everything!

Great. Make sure you have saved Program.cs before running the server.

ⓧ I'd like to double check the full code

Here is what Program.cs should look like now:

using Microsoft.EntityFrameworkCore;
using UrlShortener.Data;
using UrlShortener.Models;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

And here is Properties/launchSettings.json:

{
  "profiles": {
    "http": {
      "applicationUrl": "http://localhost:5000"
    }
  }
}

Before you run this, what do you think you will see when you visit http://localhost:5000 in your browser?

  • Start the server by running:
dotnet run
  • Open your browser and visit http://localhost:5000.

You should see the text Hello World! displayed in the browser. That confirms your clean minimal API is running on the port you configured.

  • Back in your terminal, press Ctrl+C to stop the server.

Seeing a build error instead of Hello World?

If the terminal shows a compile error, make sure you replaced the entire contents of Program.cs and did not leave any leftover weather forecast code. The file should contain only the lines shown above (the usings, builder, app, endpoint, and run call).

Help me fix this build error

Your environment is ready and your server is alive. Next up, you will define a data model and create a real database to store your shortened URLs.

Define the Data Model and Create Your Database

Your project runs and serves "Hello World" at http://localhost:5000. That is a start, but a URL shortener needs to remember which short codes map to which long URLs.

Right now, everything lives in memory. Restart the app and every link is gone. In this step you will define a data model using Entity Framework Core and store your data in a SQLite database file that survives restarts.

In this step, get ready to:
  • Create a C# model class that defines your database table shape.
  • Create a database context and register it with dependency injection.
  • Generate and apply your first database migration.
Create the ShortenedUrl model class

In EF Core, a plain C# class with public properties defines the shape of a database table. Each property becomes a column.

  • Create a Models folder in your project by running this command in the VS Code terminal:
mkdir Models
  • Create a new file called ShortenedUrl.cs inside the Models folder. In VS Code, right-click the Models folder in the Explorer sidebar and select New File.
  • Name the file ShortenedUrl.cs and add the following code:
namespace UrlShortener.Models;

public class ShortenedUrl
{
    public int Id { get; set; }
    public string LongUrl { get; set; } = string.Empty;
    public string ShortCode { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

What does this code do?

This class defines the shape of a single row in your future ShortenedUrls database table.

  • Id is the primary key. EF Core auto-increments it for you.
  • LongUrl stores the original URL the user wants to shorten.
  • ShortCode is the random code that maps to the long URL.
  • CreatedAt records when the link was created, defaulting to UTC now.
  • Save Models/ShortenedUrl.cs.
  • Confirm the project still compiles by running:
dotnet build

You should see a "Build succeeded" message with no errors.

Seeing a build error?

  • Check that your file is named exactly ShortenedUrl.cs (capital S, capital U) and sits inside the Models folder.
  • Verify the namespace line reads namespace UrlShortener.Models; with a semicolon at the end.

Help me debug a build error after creating my ShortenedUrl model class.

✔️ Awesome, I've got everything!

Great. Make sure you saved Models/ShortenedUrl.cs before moving on.

ⓧ I'd like to double check the full code

namespace UrlShortener.Models;

public class ShortenedUrl
{
    public int Id { get; set; }
    public string LongUrl { get; set; } = string.Empty;
    public string ShortCode { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
Create the AppDbContext and register it with dependency injection

A DbContext is the bridge between your C# classes and the database. It tells EF Core which tables to manage and how to connect.

  • Create a Data folder by running:
mkdir Data
  • Inside the Data folder, create a new file called AppDbContext.cs and add the following code:
using Microsoft.EntityFrameworkCore;
using UrlShortener.Models;

namespace UrlShortener.Data;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    public DbSet<ShortenedUrl> ShortenedUrls => Set<ShortenedUrl>();
}

What does this code do?

  • AppDbContext inherits from DbContext, which is EF Core's session with the database.
  • The constructor accepts DbContextOptions so the framework can pass in the connection details through dependency injection.
  • ShortenedUrls is a DbSet property. It represents the ShortenedUrls table in your database. You query and save data through it.
  • Save Data/AppDbContext.cs.
  • Now register this context in your app. In Program.cs, find this line:
var app = builder.Build();
  • Add the following line directly above it, so EF Core knows to use SQLite:
builder.Services.AddSqlite<AppDbContext>("Data Source=UrlShortener.db");

What does AddSqlite do?

AddSqlite<AppDbContext>("Data Source=UrlShortener.db") registers your AppDbContext with the dependency injection container and tells it to store data in a local file called UrlShortener.db. No separate database server needed.

  • Save Program.cs and confirm everything compiles by running:
dotnet build

You should see "Build succeeded" with no errors.

Build failed after adding AppDbContext?

  • Make sure the using UrlShortener.Data; line already exists at the top of Program.cs. It was added in Step 1.
  • Check that builder.Services.AddSqlite<AppDbContext>("Data Source=UrlShortener.db"); appears between the var builder line and the var app line.
  • Verify Data/AppDbContext.cs is saved and uses the namespace UrlShortener.Data.

Help me fix a build error after adding AppDbContext and registering it in Program.cs.

✔️ Awesome, I've got everything!

Great. Make sure both Data/AppDbContext.cs and Program.cs are saved.

ⓧ I'd like to double check the full code

Here is Data/AppDbContext.cs:

using Microsoft.EntityFrameworkCore;
using UrlShortener.Models;

namespace UrlShortener.Data;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    public DbSet<ShortenedUrl> ShortenedUrls => Set<ShortenedUrl>();
}

Here is Program.cs:

using Microsoft.EntityFrameworkCore;
using UrlShortener.Data;
using UrlShortener.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlite<AppDbContext>("Data Source=UrlShortener.db");

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
Generate and apply the initial database migration

A migration is a set of instructions that tells EF Core how to create or update the database schema to match your model. You generate it from the command line.

  • Generate your first migration by running:
dotnet ef migrations add InitialCreate

You should see output ending with "Done. To undo this action, use 'ef migrations remove'". A new Migrations folder appears in your project with generated files.

What did this command do?

EF Core compared your model (the ShortenedUrl class) against an empty database and generated C# code that creates the ShortenedUrls table with the right columns. The migration name InitialCreate works like a commit message describing what changed.

Before you apply the migration, what do you think will happen? Will a new file appear in your project folder?

  • Apply the migration and create the database by running:
dotnet ef database update
  • Confirm the database file was created by running:
ls UrlShortener.db

You should see UrlShortener.db listed. This is your SQLite database file. It contains a ShortenedUrls table ready to store data.

Don't see UrlShortener.db?

  • Make sure you ran dotnet ef database update (not just dotnet ef migrations add). The first command generates migration files. The second command actually creates the database.
  • Check that your Program.cs has the AddSqlite line with the connection string "Data Source=UrlShortener.db". If EF Core cannot find the connection info, it cannot create the file.
  • Ensure you are running the command from the project root (the folder containing UrlShortener.csproj).

Help me figure out why my UrlShortener.db file was not created after running dotnet ef database update.

Your database is ready. You now have a real, persistent store that remembers data across restarts. Next up, you will build the POST endpoint that actually creates short links and saves them to this database.

Build the Shorten Endpoint

Your database is ready with a ShortenedUrls table waiting for data. Now it is time to build the core feature of your URL shortener: the endpoint that accepts a long URL, generates a short code, stores the mapping, and returns a shortened link.

This is where your API starts doing real work. You will write your first POST endpoint using async/await to save data to SQLite through Entity Framework Core.

In this step, get ready to:
  • Write a helper method that generates random short codes.
  • Create a POST /shorten endpoint that saves URLs to your database.
  • Test the endpoint with a real HTTP request and confirm it returns a shortened link.
Write a helper method to generate short codes

Every short link needs a unique code (like abc123) that maps to the original URL. You need a method that generates a random string of lowercase letters and digits each time a new URL is shortened.

  • In VS Code, open Program.cs.
  • Add the following code below the app.Run(); line at the end of the file:
static string GenerateShortCode()
{
    // Character set: lowercase letters and digits
    const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
    var code = new char[7];
    for (int i = 0; i < 7; i++)
    {
        // Pick a random character from the set
        code[i] = chars[Random.Shared.Next(chars.Length)];
    }
    return new string(code);
}

// Define the shape of the incoming POST request body
public record ShortenRequest(string Url);

What does this code do?

GenerateShortCode() picks 7 random characters from a pool of lowercase letters and digits. This gives you over 78 billion possible combinations, so collisions are extremely unlikely for a learning project.

The ShortenRequest record tells ASP.NET Core what shape the incoming JSON should have. When someone POSTs {"url": "https://example.com"}, ASP.NET Core automatically maps that JSON into a ShortenRequest object with the Url property filled in.

  • Save Program.cs.
Create the POST /shorten endpoint

The helper method generates codes, but nothing calls it yet. Now you will wire up an HTTP endpoint that receives a URL, validates it, generates a code, stores everything in the database, and returns the short link.

  • In Program.cs, find this line:
app.MapGet("/", () => "Hello World!");
  • Replace that entire line with the following POST endpoint:
app.MapPost("/shorten", async (ShortenRequest request, AppDbContext db, HttpRequest httpRequest) =>
{
    // Reject empty or whitespace-only URLs
    if (string.IsNullOrWhiteSpace(request.Url))
        return Results.BadRequest(new { error = "URL is required." });

    // Generate a unique 7-character short code
    var shortCode = GenerateShortCode();

    // Create a new database entity
    var shortenedUrl = new ShortenedUrl
    {
        LongUrl = request.Url,
        ShortCode = shortCode,
        CreatedAt = DateTime.UtcNow
    };

    // Save to the database
    db.ShortenedUrls.Add(shortenedUrl);
    await db.SaveChangesAsync();

    // Build the full short URL and return it
    var shortUrl = $"{httpRequest.Scheme}://{httpRequest.Host}/{shortCode}";
    return Results.Created(shortUrl, new { shortUrl });
});

What does this endpoint do?

ASP.NET Core's dependency injection automatically provides AppDbContext and HttpRequest as parameters. You registered AppDbContext in the previous step with AddSqlite, so the framework knows how to create one for each request.

The await db.SaveChangesAsync() call writes the new record to your SQLite database without blocking the server. The Results.Created method returns an HTTP 201 status code, which is the standard response for "I created a new resource for you."

  • Save Program.cs.

Seeing red squiggles in VS Code?

If you see errors under ShortenRequest or GenerateShortCode, make sure the helper method and record from the previous chunk are placed below app.Run(); in the same file. C# top-level statements require type definitions and local functions to appear after all executable statements.

help me fix red squiggles in my Program.cs file

Test it with curl

Your endpoint is wired up. Time to send a real HTTP request and see if your API shortens a URL and stores it in the database.

  • Start your application by running this command:
dotnet run

Before you send your first request, think about this: what do you expect the response to look like when you POST a URL to /shorten?

  • Open a second terminal in VS Code by clicking the + icon in the terminal panel.
  • Send a POST request to your API by running this command:

macOS

curl -X POST "http://localhost:5000/shorten" -H "Content-Type: application/json" -d "{\"url\": \"https://learn.microsoft.com\"}"

Windows

Invoke-RestMethod -Method Post -Uri "http://localhost:5000/shorten" -ContentType "application/json" -Body '{"url": "https://learn.microsoft.com"}'

You should see a JSON response containing a shortUrl like http://localhost:5000/abc123 (your 7-character code will be different).

What is that short code?

The random string at the end of your URL is the code your GenerateShortCode() method created. Every time you POST a URL, a new unique code is generated and stored in your SQLite database alongside the original long URL.

  • Send a second POST with a different URL to confirm you get a different short code:

macOS

curl -X POST "http://localhost:5000/shorten" -H "Content-Type: application/json" -d "{\"url\": \"https://github.com\"}"

Windows

Invoke-RestMethod -Method Post -Uri "http://localhost:5000/shorten" -ContentType "application/json" -Body '{"url": "https://github.com"}'

You should see a second JSON response with a different 7-character code than the first. This confirms your API generates unique codes for each URL.

Getting a 404 or connection refused?

Make sure dotnet run is still active in your first terminal. If you stopped it, run dotnet run again before sending the request.

Check that your URL is http://localhost:5000/shorten (not https and not a different port). If you see a 400 error, make sure your JSON body includes the url key with a non-empty value.

help me debug why my POST request to /shorten is failing

  • Stop the server by pressing Ctrl+C in the first terminal.

✔️ Awesome, I've got everything!

Great. Double check you have saved Program.cs and confirmed both POST requests returned different short codes.

ⓧ I'd like to double check the full code

Your Program.cs should now have these sections in order. Scroll through your file and confirm each one is present:

  • The three using statements at the top (Microsoft.EntityFrameworkCore, UrlShortener.Data, UrlShortener.Models).
  • The builder and app setup lines including AddSqlite<AppDbContext>.
  • The app.MapPost("/shorten", ...) endpoint (replacing the old Hello World route).
  • app.Run(); on its own line.
  • The GenerateShortCode() static method below app.Run().
  • The public record ShortenRequest(string Url); line at the very bottom.

Your API now accepts long URLs and returns short links. Next up, you will make those short links actually work by building a redirect endpoint that sends visitors to the original URL.

Build the Redirect Endpoint

Your POST endpoint creates short links and stores them in the database. But right now those links are dead ends.

If you paste one of your short URLs into a browser, you get a 404 because no endpoint handles the /{shortCode} route yet. This step adds the redirect endpoint that makes your short links actually work.

In this step, get ready to:
  • Map a GET endpoint that looks up short codes in the database.
  • Return an HTTP 301 redirect that sends browsers to the original URL.
  • Test the full flow by creating a short link and visiting it in your browser.
Add the redirect endpoint to Program.cs

When someone visits a short URL, your API needs to look up the matching record in the database and tell the browser where to go. This is done with an HTTP redirect response.

  • In Program.cs, find the closing }); of the MapPost("/shorten", ...) endpoint.
  • Add the following GET endpoint directly below it:
app.MapGet("/{shortCode}", async (string shortCode, AppDbContext db) =>
{
    // Look up the short code in the database
    var shortenedUrl = await db.ShortenedUrls.FirstOrDefaultAsync(s => s.ShortCode == shortCode);

    // Return 404 if no matching record exists
    if (shortenedUrl is null)
        return Results.NotFound(new { error = "Short link not found." });

    // Redirect the browser to the original long URL
    return Results.Redirect(shortenedUrl.LongUrl, permanent: true);
});

What does this code do?

This endpoint catches any GET request with a path like /abc123 and treats the path segment as a short code.

  • FirstOrDefaultAsync searches the ShortenedUrls table for a row where ShortCode matches. If nothing is found, it returns null.
  • Results.NotFound sends back an HTTP 404 status with a JSON error message.
  • Results.Redirect(..., permanent: true) sends an HTTP 301 response. The browser reads the redirect header and automatically navigates to the original URL.

Why permanent: true?

A 301 status code means "Moved Permanently." Browsers and search engines cache this redirect, which is the standard behaviour for URL shorteners. Once a short link is created, it always points to the same destination.

  • Save Program.cs.
Test the full flow end-to-end

Time to prove your short links actually redirect. You will create a fresh short link, then visit it in your browser.

  • Stop the previous server if it is still running by pressing Ctrl+C in the terminal.
  • Start the app by running:
dotnet run
  • Create a new short link by running this command in a second terminal:
curl -X POST "http://localhost:5000/shorten" -H "Content-Type: application/json" -d "{\"url\": \"https://learn.microsoft.com\"}"

You receive a JSON response with a shortUrl value like http://localhost:5000/abc123 (your short code will be different).

Before you paste that short URL into your browser, what do you think will happen? Will the browser stay on localhost, or end up somewhere else?

  • Copy the shortUrl from the response and paste it into your browser's address bar.
  • Press Enter.

Your browser redirects you to https://learn.microsoft.com. The address bar changes to the Microsoft Learn homepage. That is your redirect working end-to-end.

  • Test the 404 case by visiting a fake short code in your browser:
curl http://localhost:5000/doesnotexist

You see a JSON response: {"error":"Short link not found."} with a 404 status.

Browser not redirecting?

  • Make sure you copied the full URL including http://localhost:5000/ and the short code. A partial paste will not reach your server.
  • If you see a port-in-use error when starting the app, your previous instance is still running. Press Ctrl+C in the first terminal to stop it, then run dotnet run again.
  • If you get a 404 for a code you just created, double-check that the MapGet("/{shortCode}", ...) block is placed after the MapPost block and before app.Run().

Help me debug why my redirect endpoint is not working.

✔️ Awesome, I've got everything!

Great. Double check you have saved Program.cs and your app is running.

ⓧ I'd like to double check the full code

Your Program.cs is now over 30 lines, so confirm these sections are present in this order:

  • The three using statements at the top (Microsoft.EntityFrameworkCore, UrlShortener.Data, UrlShortener.Models).
  • The builder and AddSqlite<AppDbContext> registration.
  • The MapPost("/shorten", ...) endpoint from the previous step.
  • The new MapGet("/{shortCode}", ...) endpoint you just added.
  • app.Run() after both endpoints.
  • The GenerateShortCode() static method and the ShortenRequest record at the bottom.

Your short links are alive. Paste any of them into a browser and you land on the original page. Next up, you will add a listing endpoint and input validation to round out your API.

Add a List Endpoint and Handle Edge Cases

Your short links work end-to-end now. You can create them and follow them in a browser.

But there are two gaps. You have no way to see all the links you have created, and nothing stops a user from submitting garbage like not-a-url as input. This step rounds out your API into something you could confidently show in an interview: a listing endpoint, input validation, and duplicate handling.

In this step, get ready to:
  • Create a GET /urls endpoint that returns all shortened URLs as JSON.
  • Add URL format validation to the POST /shorten endpoint.
  • Handle duplicate URLs by returning the existing short code.
Create a GET /urls endpoint

Right now, the only way to know what short links exist is to remember the curl output from when you created them. A listing endpoint fixes that by querying the database and returning every record as a JSON array.

  • Stop the running server by pressing Ctrl+C in your terminal.
  • In Program.cs, find the GET /{shortCode} endpoint (the one with Results.Redirect).
  • Add this new endpoint ABOVE that route by pasting the following code between the closing }); of the POST /shorten endpoint and the app.MapGet("/{shortCode}" line:
app.MapGet("/urls", async (AppDbContext db) =>
{
    // Query all shortened URLs from the database
    var urls = await db.ShortenedUrls
        .AsNoTracking()
        .Select(s => new
        {
            s.ShortCode,
            s.LongUrl,
            s.CreatedAt
        })
        .ToListAsync();

    // Return the list as a JSON array
    return Results.Ok(urls);
});

What does this code do?

This endpoint queries every row in the ShortenedUrls table and returns them as JSON.

  • AsNoTracking() tells EF Core to skip change tracking for this query. Since you are only reading data (not updating it), this improves performance. It is a best practice worth mentioning in interviews.
  • Select() projects each row into an anonymous object with only the fields you want to expose. The Id column stays hidden from the API consumer.
  • Results.Ok() returns an HTTP 200 response with the list serialized as JSON.

Why does /urls go above /{shortCode}?

ASP.NET Core matches literal routes like /urls before parameterized routes like /{shortCode}. Placing /urls first makes the intent clear and avoids any ambiguity about route priority.

  • Save Program.cs.
  • Start the server again by running:
dotnet run
  • Test the new endpoint by running:
curl http://localhost:5000/urls

You should see a JSON array listing the short links you created in previous steps, each with a shortCode, longUrl, and createdAt field.

Seeing an empty array?

If the response is [], it means your database has no rows. This can happen if you deleted and re-created the database file between steps. Create a short link first with a POST request, then try /urls again.

If you see a 404 or the response looks wrong, confirm that the app.MapGet("/urls", ...) call appears before the app.MapGet("/{shortCode}", ...) call in your file.

Help me debug my /urls endpoint

Add input validation to the POST endpoint

Your POST endpoint currently accepts any string as a URL. Someone could submit hello or 123 and your API would happily shorten it. Real APIs validate that the input is a properly formatted absolute URL before storing it.

  • In Program.cs, find the existing whitespace check inside the POST /shorten endpoint:
if (string.IsNullOrWhiteSpace(request.Url))
    return Results.BadRequest(new { error = "URL is required." });
  • Add this URL format validation directly below that check:
// Validate the URL is a proper absolute URL (e.g. https://example.com)
if (!Uri.TryCreate(request.Url, UriKind.Absolute, out _))
    return Results.BadRequest(new { error = "Invalid URL format. Must be an absolute URL." });

What does Uri.TryCreate do?

Uri.TryCreate is a built-in .NET method that attempts to parse a string into a URI object. The UriKind.Absolute argument means it only accepts full URLs with a scheme (like https://). Relative paths like /page or bare words like hello will fail.

The out _ discards the parsed result since you only care whether parsing succeeded.

  • Save Program.cs.
  • Stop the server with Ctrl+C and restart it by running:
dotnet run
  • Test the validation by sending an invalid URL:
curl -X POST "http://localhost:5000/shorten" -H "Content-Type: application/json" -d "{\"url\": \"not-a-url\"}"

You should see a 400 Bad Request response with the message Invalid URL format. Must be an absolute URL. in the JSON body.

Still getting a 201 Created?

If the invalid URL was accepted and shortened, double-check that your new Uri.TryCreate check appears after the whitespace check and before the var shortCode = GenerateShortCode(); line. Both checks must be at the top of the endpoint, before any database operations.

Help me fix my URL validation

Handle duplicate URLs gracefully

If a user shortens the same URL twice, your API currently creates two separate entries with different short codes. A polished API detects the duplicate and returns the existing short code instead of wasting database space.

  • In Program.cs, find the Uri.TryCreate validation you just added.
  • Add this duplicate check directly below it (before the var shortCode = GenerateShortCode(); line):
// Check if this URL has already been shortened
var existing = await db.ShortenedUrls.FirstOrDefaultAsync(s => s.LongUrl == request.Url);
if (existing is not null)
{
    // Return the existing short URL instead of creating a duplicate
    var existingShortUrl = $"{httpRequest.Scheme}://{httpRequest.Host}/{existing.ShortCode}";
    return Results.Ok(new { shortUrl = existingShortUrl });
}

What does this code do?

FirstOrDefaultAsync() searches the database for any row whose LongUrl matches the incoming request. If it finds one, the endpoint returns the already-existing short URL with a 200 OK instead of creating a new row.

This is the is not null pattern in C#. If the query returns no match, existing is null and the code falls through to the normal creation flow below.

  • Save Program.cs.
  • Stop the server with Ctrl+C and restart it by running:
dotnet run
  • Test by shortening the same URL twice. Run this command:
curl -X POST "http://localhost:5000/shorten" -H "Content-Type: application/json" -d "{\"url\": \"https://example.com\"}"
  • Run the exact same command again:
curl -X POST "http://localhost:5000/shorten" -H "Content-Type: application/json" -d "{\"url\": \"https://example.com\"}"

Both responses should contain the same shortUrl value. The second request found the existing record and returned it instead of generating a new code.

Getting different short codes each time?

If you get a different short code on the second POST, the duplicate check is not running before code generation. Make sure your var existing = await db.ShortenedUrls.FirstOrDefaultAsync(...) block appears AFTER the two validation checks and BEFORE the var shortCode = GenerateShortCode(); line.

Help me debug my duplicate URL check

Verify the complete API

Before you run the final check, predict what you expect to see. If you call GET /urls now, how many entries do you think will be in the array?

  • Confirm your API returns all stored links by running:
curl http://localhost:5000/urls

You should see a JSON array listing all your shortened URLs, each with shortCode, longUrl, and createdAt fields. Notice that https://example.com appears only once, proving your duplicate handling works.

✔️ Awesome, I've got everything!

Great. Double check you have saved Program.cs and that all three pieces are in place: the /urls endpoint, the Uri.TryCreate validation, and the duplicate check.

ⓧ I'd like to double check the full code

Scroll through your Program.cs and confirm these sections appear in order:

  • The using statements and builder.Services.AddSqlite registration at the top.
  • The POST /shorten endpoint with: whitespace check, Uri.TryCreate check, duplicate FirstOrDefaultAsync check, then code generation and save.
  • The GET /urls endpoint with AsNoTracking() and Select projection.
  • The GET /{shortCode} redirect endpoint (unchanged from the previous step).
  • The GenerateShortCode() static method and the ShortenRequest record at the bottom.

If anything is out of order or missing, scroll back up to the relevant substep and compare your code against the code block shown there.

Your API now lists all links, validates input, and handles duplicates. You have a production-quality URL shortener running on your machine. Next up, you will take on the Secret Mission and add click tracking to see how many times each short link gets visited.

Secret mission

Add Click Tracking

Real URL shorteners like Bitly track how many times each link gets clicked. Can you add analytics to your API? You will modify your database schema, create a new migration, and update your redirect endpoint to count every visit.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources for future tinkering, or delete them entirely.

Resources you used:

  • The UrlShortener project folder on your Desktop (includes source code, the UrlShortener.db SQLite database, and migration files).
  • The dotnet-ef global tool installed on your machine.

Keep everything running

No action needed. Choose this if you plan to keep building on this project or want it as a reference.

  • Your project folder stays on your Desktop, ready to run again with dotnet run at any time.
  • The dotnet-ef tool stays installed globally. It is useful for any future Entity Framework Core project.
  • The UrlShortener.db file is a local SQLite database that takes up minimal disk space.

Pause - I'll come back to this later

If you have the dev server running, stop it to free up your terminal. Your files stay on disk for when you return.

  • In the terminal where dotnet run is active, press Ctrl+C to stop the server.
  • Everything else stays in place. When you come back, open the UrlShortener folder in VS Code and run dotnet run again.

Delete - I don't want to use this again

Remove all project files and the global tool to start fresh.

  • Stop the dev server if it is still running by pressing Ctrl+C in the terminal.

Delete the project folder on macOS/Linux by running:

cd ~/Desktop
rm -rf UrlShortener

Delete the project folder on Windows PowerShell by running:

cd ~\Desktop
Remove-Item -Recurse -Force UrlShortener

Uninstall the dotnet-ef global tool by running:

dotnet tool uninstall --global dotnet-ef

Why keep dotnet-ef?

The dotnet-ef tool is useful for any future project that uses Entity Framework Core. If you expect to work with databases in .NET again, you can skip this uninstall step.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully functional URL shortener API from scratch using ASP.NET Core and .NET 10.

You've learned how to:

  • Built a REST API using ASP.NET Core Minimal APIs, handling real HTTP verbs (GET, POST) and status codes (200, 201, 301, 400, 404) that power the web.
  • Persisted data across application restarts using Entity Framework Core with a SQLite database, including writing migrations and querying with async/await.
  • Implemented URL redirection end-to-end, the core mechanism behind every URL shortener, with input validation and duplicate handling for production-quality code.
  • (Secret Mission) Added click tracking analytics by modifying an existing EF Core model, creating a new database migration on top of a live schema, and performing atomic update operations.

Ready to quiz yourself?