Build a REST API with ASP.NET Core
Build a Bookmark Manager API with full CRUD using C# and .NET 10.
Introduction
30 Second Summary
Every app on your phone talks to a server somewhere. When you check the weather, scroll social media, or save a bookmark, your app is sending requests to an API and getting data back.
In this project, you will build a REST API from scratch using ASP.NET Core and C#. You will create, read, update, and delete bookmarks through HTTP requests, and test everything through an interactive browser page.
What You'll Build
You will open a browser, see a live testing page, and create bookmarks like "Learn C# - https://learn.microsoft.com". You will get them back as JSON, update their titles, and delete them, all without writing a single line of frontend code.
By the end of this project, you'll have:
- A running API that accepts POST, GET, PUT, and DELETE requests and responds with JSON bookmark data.
- Full CRUD operations backed by a database using Entity Framework Core.
- A Swagger UI page where you can interactively test every endpoint from your browser.
- Secret Mission: Add a search endpoint that filters bookmarks by title keyword.
Are there any prerequisites?
You should be comfortable with basic programming concepts like variables, loops, and functions. No prior C# or API experience is needed.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters. REST APIs are the backbone of almost every web and mobile application. Understanding how to build one is one of the most direct paths to a backend development role.
Set Up Your Development Environment
Before you can write any C# code, you need two things on your machine: the .NET SDK (which compiles and runs your code) and a code editor configured for C#.
In this step, you'll install the .NET 10 SDK, set up Visual Studio Code, and add the C# Dev Kit extension that gives you IntelliSense and debugging support.
In this step, get ready to:
- Install the .NET 10 SDK on your machine.
- Install Visual Studio Code with the C# Dev Kit extension.
- Verify your setup by running a version check.
Install the .NET 10 SDK
The .NET SDK is what lets you compile and run C# projects from the command line. This project uses .NET 10, which is the current Long-Term Support release.
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
- Check if .NET is already installed by running:
dotnet --version
Look at what appears in your terminal and pick the tab that matches:
✔️ I see version 10 or higher
Your .NET 10 SDK is already installed and ready to go. Skip ahead to the next substep.
ⓧ I see an older version
You have an older version of .NET installed. .NET supports side-by-side installations, so you can add .NET 10 without removing your existing version.
- Install the .NET 10 SDK by running:
winget install Microsoft.DotNet.SDK.10
- After the installation completes, close your PowerShell window completely.
- Open a new PowerShell window so the PATH updates take effect.
- Confirm the installation succeeded by running:
dotnet --version
You should now see a version number starting with 10.
ⓧ Command not found
You don't have .NET installed yet. You'll install it using Windows Package Manager.
- Install the .NET 10 SDK by running:
winget install Microsoft.DotNet.SDK.10
- After the installation completes, close your PowerShell window completely.
- Open a new PowerShell window so the PATH updates take effect.
- Confirm the installation succeeded by running:
dotnet --version
You should now see a version number starting with 10.
Seeing 'winget' is not recognized?
The winget command comes pre-installed on Windows 10 version 1809 and later. If your system doesn't have it, open the Microsoft Store, search for App Installer, and install it.
Alternatively, download the .NET 10 SDK directly from the official .NET download page and run the installer.
Help me troubleshoot my .NET SDK installation.
Install Visual Studio Code and the C# Dev Kit extension
Visual Studio Code is a lightweight code editor. The C# Dev Kit extension adds IntelliSense, debugging, and project management features specifically for .NET development.
- Download Visual Studio Code from https://code.visualstudio.com and run the installer.
- Once installed, press the Windows key to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the Extensions panel by pressing Ctrl+Shift+X.
- Type C# Dev Kit in the search bar at the top of the panel.
- Find the extension published by Microsoft and click Install.
You should see the C# Dev Kit extension appear in your installed extensions list with a green checkmark.
Extension not showing up in search?
Make sure you're searching in the Extensions panel (the square icon in the left sidebar), not the main file search. The extension name is exactly C# Dev Kit and the publisher should say Microsoft.
Help me find and install the C# Dev Kit extension in VS Code.
Verify your full setup
Now that you have both VS Code and the .NET SDK installed, confirm that VS Code's integrated terminal can find the .NET SDK.
- Open the integrated terminal in VS Code by pressing Ctrl+` (that's the backtick key, above Tab).
Before you run the next command, think about what you expect to see. If your installation went smoothly, what should the output look like?
- Run this command in the VS Code terminal:
dotnet --version
You should see a version number starting with 10 (for example, 10.0.302). This confirms both the .NET SDK and VS Code's terminal are working together.
VS Code terminal not finding dotnet?
If the VS Code terminal shows an error, close VS Code completely and reopen it. VS Code loads the system PATH when it starts, so it needs a restart to pick up the new .NET installation.
Help me fix my VS Code terminal so it recognizes the dotnet command.
Your development environment is ready. Next up, you'll create your first API project and see it respond to a request in the browser.
Create Your API Project and See It Run
Your development environment is ready to go.
Now you need a running project to build on. ASP.NET Core comes with a project template that includes a sample API endpoint. By getting that sample to respond in your browser, you'll prove the entire toolchain works before writing any custom code.
In this step, get ready to:
- Create a new ASP.NET Core Web API project with a sample controller.
- Run the project and confirm it returns JSON data in your browser.
- Explore the project file structure in VS Code.
Create a new API project
The .NET SDK includes ready-made project templates that scaffold a working app for you. You'll use the webapi template to create a project that already has a sample controller and endpoint wired up.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create a new Web API project and move into its folder by running these commands:
dotnet new webapi --use-controllers -o BookmarkApi
cd BookmarkApi
What do these flags mean?
- webapi is the template name for a Web API project.
- --use-controllers tells the template to use controller-based routing instead of minimal APIs.
- -o BookmarkApi names the output folder where all the project files land.
- Confirm the project was created by running:
ls
You should see files like Program.cs, BookmarkApi.csproj, and folders like Controllers and Properties.
Don't see these files?
- Make sure you ran cd BookmarkApi after the dotnet new command. Without that, you're still in the Desktop folder looking at the outer directory.
- If the dotnet new command failed, check that your terminal is PowerShell (not a restricted shell) and that you closed and reopened it after installing the .NET SDK.
Help me figure out why my project wasn't created.
Run the project and see it respond
The scaffolded project includes a sample WeatherForecast endpoint that returns fake weather data. Running the project now proves your entire setup works end to end.
- Start the project by running this command:
dotnet run --launch-profile https
What to expect
This command starts a local web server and takes over your terminal. You won't be able to type more commands in this terminal until you stop the server later.
The terminal prints a URL like https://localhost:7xxx where 7xxx is a randomly assigned port number. Note down this URL. You'll need it in a moment.
Before you open the browser, think about what you expect to see at /weatherforecast. What would a sample weather API return?
- Open your browser and navigate to the URL from the terminal output with /weatherforecast appended. For example, if the terminal shows https://localhost:7042, go to https://localhost:7042/weatherforecast.
You see a JSON array of randomly generated weather data with fields like temperatureC, temperatureF, and summary. This confirms your REST API is running and responding to HTTP requests.
Seeing a certificate error or connection refused?
- If your browser shows a certificate warning, your development certificate may not be trusted yet. Open a second terminal and run dotnet dev-certs https --trust, then refresh the page.
- If you see "connection refused", double-check that the port in your browser URL matches the port shown in the terminal output.
- Make sure you're using https:// (not http://) since the https launch profile only serves on the HTTPS port.
Help me fix my connection issue.
Stop the app and explore the file structure
- Press Ctrl+C in your terminal to stop the running server.
- Open the project folder in VS Code by running this command:
code .
VS Code opens with the BookmarkApi project loaded. In the Explorer sidebar on the left, locate these three key files you'll work with in upcoming steps:
- Program.cs is the app entry point where services are registered and middleware is configured.
- Controllers/WeatherForecastController.cs is the sample controller that handled the request you just made in the browser.
- Properties/launchSettings.json defines the port numbers and launch profiles for your project.
Why controllers?
ASP.NET Core offers two styles for building APIs: controllers and minimal APIs. Controllers use classes decorated with attributes like [ApiController] to organize endpoints.
Most enterprise .NET codebases use controllers because they separate concerns cleanly into models, controllers, and services. Learning this pattern transfers directly to the code you'll see on the job.
VS Code not opening?
- If you see "code is not recognized", the VS Code code command is not on your PATH. Open VS Code manually using your search bar (Windows key, type VS Code), then click File > Open Folder and select the BookmarkApi folder on your Desktop.
Help me open my project in VS Code.
Your project is running and your tools are connected. Next up, you'll build your own Bookmark model and controller to replace the sample WeatherForecast endpoint.
Build Your Bookmark Model and Controller
The WeatherForecast endpoint proved your ASP.NET Core setup works. Now you need your own data shape and your own endpoint that handles real operations like creating and retrieving bookmarks.
You'll define a Bookmark model, create a controller with GET and POST endpoints, and test them. Along the way, you'll discover a fundamental limitation that reveals why real APIs need databases.
In this step, get ready to:
- Create a Bookmark model class with properties for ID, title, URL, and timestamp.
- Build a BookmarksController with GET and POST endpoints.
- Test the API and discover why data disappears between requests.
Create the Bookmark model
Every API needs a data shape. In ASP.NET Core, you define this shape as a model class. Your Bookmark model will hold the properties each bookmark needs: an ID, a title, a URL, and a timestamp.
- In VS Code's Explorer sidebar, right-click on the BookmarkApi project folder.
- Select New Folder and name it Models.
- Right-click on the new Models folder and select New File.
- Name the file Bookmark.cs and paste the following code:
namespace BookmarkApi.Models;
public class Bookmark
{
public long Id { get; set; }
public string? Title { get; set; }
public string? Url { get; set; }
public DateTime CreatedAt { get; set; }
}
What does this code do?
- namespace BookmarkApi.Models groups this class under the project's Models namespace so other files can reference it with a using statement.
- Id is a long (a large number) that uniquely identifies each bookmark.
- Title and Url are nullable strings (string?) so they can be left empty without causing errors.
- CreatedAt records when the bookmark was created.
- Save Models/Bookmark.cs.
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before moving on.
ⓧ I'd like to double check the full code
namespace BookmarkApi.Models;
public class Bookmark
{
public long Id { get; set; }
public string? Title { get; set; }
public string? Url { get; set; }
public DateTime CreatedAt { get; set; }
}
Create the Bookmarks controller
A controller is the class that handles incoming HTTP requests and decides what to send back. Your BookmarksController will store bookmarks in a simple list and expose two endpoints: one to create bookmarks and one to retrieve them.
- In the Explorer sidebar, right-click on the Controllers folder.
- Select New File and name it BookmarksController.cs.
- Paste the following code:
using Microsoft.AspNetCore.Mvc;
using BookmarkApi.Models;
namespace BookmarkApi.Controllers;
[Route("api/[controller]")]
[ApiController]
public class BookmarksController : ControllerBase
{
private readonly List<Bookmark> _bookmarks = new();
[HttpGet]
public ActionResult<IEnumerable<Bookmark>> GetBookmarks()
{
return _bookmarks;
}
[HttpPost]
public ActionResult<Bookmark> PostBookmark(Bookmark bookmark)
{
_bookmarks.Add(bookmark);
return bookmark;
}
}
What does this code do?
- [Route("api/[controller]")] sets the URL path. Since the class is called BookmarksController, the endpoint becomes /api/bookmarks.
- [ApiController] enables automatic request validation and JSON binding.
- _bookmarks is a private list that stores bookmarks in memory.
- [HttpGet] marks GetBookmarks() as the handler for GET requests. It returns the list.
- [HttpPost] marks PostBookmark() as the handler for POST requests. It adds the incoming bookmark to the list and returns it.
- Save Controllers/BookmarksController.cs.
- Verify the project compiles by running this in your terminal:
dotnet build
You should see a message ending with Build succeeded and no errors.
Seeing build errors?
- Check that Bookmark.cs is inside a folder called Models (not at the project root). The namespace must match the folder structure.
- Make sure the first line of BookmarksController.cs has using BookmarkApi.Models; so it can find the Bookmark class.
- Confirm both files are saved before running dotnet build.
Help me fix build errors in my ASP.NET Core project
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
using Microsoft.AspNetCore.Mvc;
using BookmarkApi.Models;
namespace BookmarkApi.Controllers;
[Route("api/[controller]")]
[ApiController]
public class BookmarksController : ControllerBase
{
private readonly List<Bookmark> _bookmarks = new();
[HttpGet]
public ActionResult<IEnumerable<Bookmark>> GetBookmarks()
{
return _bookmarks;
}
[HttpPost]
public ActionResult<Bookmark> PostBookmark(Bookmark bookmark)
{
_bookmarks.Add(bookmark);
return bookmark;
}
}
Test the API and observe the problem
Your controller compiles, but does it actually work? You'll create an .http file to send test requests, then run the app to see what happens.
- First, find your HTTPS port. In the Explorer sidebar, open Properties/launchSettings.json.
- Look for the https profile and find the applicationUrl value. Note the port number (the number after localhost:). It will be somewhere between 7000 and 7300.
Why do I need to check the port?
ASP.NET Core randomly assigns a port between 7000 and 7300 when you create a project. Your port will be different from any examples you see online. You need to use your specific port so requests reach your running app.
- Right-click on the BookmarkApi project root in the Explorer sidebar.
- Select New File and name it BookmarkApi.http.
- Paste the following code, replacing the port number with your own:
@BookmarkApi_HostAddress = https://localhost:[[HTTPS_PORT="your HTTPS port (e.g. 7001)"]]
POST {{BookmarkApi_HostAddress}}/api/bookmarks
Content-Type: application/json
{
"title": "Learn C#",
"url": "https://learn.microsoft.com/dotnet/csharp"
}
###
GET {{BookmarkApi_HostAddress}}/api/bookmarks
###
What does this file do?
- The @BookmarkApi_HostAddress line defines a variable so you only type your URL once.
- The POST request sends a JSON bookmark (with a title and URL) to your API.
- The ### separates different requests in the same file.
- The GET request retrieves all bookmarks from your API.
- Save BookmarkApi.http.
- Start your app by running this command in the terminal:
dotnet run --launch-profile https
The terminal shows your app is listening on its HTTPS URL. Keep it running.
- Open the BookmarkApi.http file in VS Code.
- Click the Send Request link that appears above the POST request line.
Don't see Send Request links?
You need the REST Client extension for VS Code. Press Ctrl+Shift+X to open Extensions, search for "REST Client" by Huachao Mao, and install it. Then reopen your .http file.
The response pane opens on the right showing the bookmark JSON you just sent. The POST appears to succeed.
Before you send the GET request, think about this: what do you expect to see when you retrieve all bookmarks? The bookmark you just created, or something else?
- Click the Send Request link above the GET request line.
You see an empty array: []. The bookmark you just posted is gone.
Why is the list empty?
ASP.NET Core creates a brand new instance of your controller for every single request. When the POST request arrived, a new BookmarksController was created with a fresh empty list. The bookmark was added. Then the request ended and that controller instance was destroyed.
When the GET request arrived a moment later, a completely new BookmarksController was created with its own fresh empty list. The bookmark from the previous request no longer exists anywhere.
This is by design. Controllers have a "transient" lifetime. You need a data store that lives outside the controller and survives across requests. That is what a database provides.
- Stop the app by pressing Ctrl+C in the terminal.
✔️ Awesome, I've got everything!
Great. Make sure you have BookmarkApi.http saved with your correct port number.
ⓧ I'd like to double check the full code
@BookmarkApi_HostAddress = https://localhost:7001
POST {{BookmarkApi_HostAddress}}/api/bookmarks
Content-Type: application/json
{
"title": "Learn C#",
"url": "https://learn.microsoft.com/dotnet/csharp"
}
###
GET {{BookmarkApi_HostAddress}}/api/bookmarks
###
Your port number may differ from 7001. Use the port from your Properties/launchSettings.json file.
You've proven that a controller alone cannot hold data between requests. In the next step, you'll add a real database that persists your bookmarks across every request.
Add a Database That Persists Your Data
In the last step, you saw the fundamental problem with storing data in a controller's private field. ASP.NET Core creates a brand new controller instance for every single request, so your list of bookmarks vanishes the moment the response is sent.
You need a data store that lives outside the controller and survives across requests. Entity Framework Core with an in-memory database gives you exactly this. It provides a shared database context that Dependency Injection delivers to every controller that asks for it.
In this step, get ready to:
- Install the Entity Framework Core InMemory package.
- Create a database context class and register it with your app.
- Rewrite the controller to use the database and verify data persists.
Install Entity Framework Core InMemory and create the database context
Entity Framework Core is Microsoft's database toolkit for .NET. The InMemory provider gives you a lightweight database that lives in your app's memory. It teaches the same patterns you would use with a real database like SQL Server or PostgreSQL.
- Install the Entity Framework Core InMemory package by running this command in your terminal:
dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 10.0.8
What does this command do?
This adds the Microsoft.EntityFrameworkCore.InMemory NuGet package to your project. NuGet is .NET's package manager, similar to npm for JavaScript or pip for Python.
- Confirm the package was added by checking the terminal output. You should see a message indicating the package was successfully installed.
Seeing a version error?
Make sure you typed the version number exactly as 10.0.8. If you see a "version not found" error, check your internet connection and try again.
Help me troubleshoot this issue.
Now you need a class that represents your database. In Entity Framework Core, this is called a DbContext. It acts as a bridge between your C# code and the database.
- Create a new file called BookmarkContext.cs inside the Models folder.
- Add the following code to Models/BookmarkContext.cs:
using Microsoft.EntityFrameworkCore;
namespace BookmarkApi.Models;
// Database context that manages Bookmark entities
public class BookmarkContext : DbContext
{
public BookmarkContext(DbContextOptions<BookmarkContext> options)
: base(options)
{
}
// Represents the Bookmarks table in the database
public DbSet<Bookmark> Bookmarks { get; set; } = null!;
}
What does this code do?
The BookmarkContext class inherits from DbContext, which is Entity Framework Core's main class for interacting with a database.
The DbSet<Bookmark> property called Bookmarks represents a collection of all bookmarks in the database. You can query it, add to it, and remove from it just like a regular collection.
- Save Models/BookmarkContext.cs.
✔️ Awesome, I've got everything!
Great. Double check you saved the file, then move on to the next substep.
ⓧ I'd like to double check the full code
using Microsoft.EntityFrameworkCore;
namespace BookmarkApi.Models;
public class BookmarkContext : DbContext
{
public BookmarkContext(DbContextOptions<BookmarkContext> options)
: base(options)
{
}
public DbSet<Bookmark> Bookmarks { get; set; } = null!;
}
Register the database context in Program.cs
Your app doesn't know about BookmarkContext yet. You need to register it with ASP.NET Core's dependency injection system so it can be provided to any controller that needs it.
- Open Program.cs in VS Code.
- Replace the entire contents of Program.cs with the following code:
using Microsoft.EntityFrameworkCore;
using BookmarkApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOpenApi();
// Register the database context with an in-memory database
builder.Services.AddDbContext<BookmarkContext>(opt =>
opt.UseInMemoryDatabase("BookmarkStore"));
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
What does this code do?
The two using statements at the top import Entity Framework Core and your Models namespace so the compiler can find BookmarkContext.
The AddDbContext line registers your database context with ASP.NET Core's dependency injection container. It tells the framework to create a BookmarkContext backed by an in-memory database named BookmarkStore and hand it to any controller that requests one.
- Save Program.cs.
✔️ Awesome, I've got everything!
Great. Double check you saved the file, then move on to rewriting the controller.
ⓧ I'd like to double check the full code
using Microsoft.EntityFrameworkCore;
using BookmarkApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.Services.AddDbContext<BookmarkContext>(opt =>
opt.UseInMemoryDatabase("BookmarkStore"));
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Rewrite the controller to use the database
Now for the big payoff. Instead of a private list that resets every request, your controller will receive the shared database context through its constructor. This pattern is called constructor injection.
- Open Controllers/BookmarksController.cs in VS Code.
- Select all the code and delete it. Replace it with this first section (the class setup, constructor, and GET method):
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using BookmarkApi.Models;
namespace BookmarkApi.Controllers;
[Route("api/[controller]")]
[ApiController]
public class BookmarksController : ControllerBase
{
// Database context injected by ASP.NET Core
private readonly BookmarkContext _context;
public BookmarksController(BookmarkContext context)
{
_context = context;
}
// Return all bookmarks from the database
[HttpGet]
public async Task<ActionResult<IEnumerable<Bookmark>>> GetBookmarks()
{
return await _context.Bookmarks.ToListAsync();
}
What's happening here?
The constructor now accepts a BookmarkContext parameter. ASP.NET Core automatically provides this when creating the controller. No more private readonly List<Bookmark> field that resets every request.
The GetBookmarks method is now async because database operations take time. It uses ToListAsync() to fetch all bookmarks from the shared database.
- Directly below the GetBookmarks method (still inside the class), add the GET-by-ID and POST methods:
// Return a single bookmark by its ID
[HttpGet("{id}")]
public async Task<ActionResult<Bookmark>> GetBookmark(long id)
{
var bookmark = await _context.Bookmarks.FindAsync(id);
if (bookmark == null)
{
return NotFound();
}
return bookmark;
}
// Create a new bookmark and save it to the database
[HttpPost]
public async Task<ActionResult<Bookmark>> PostBookmark(Bookmark bookmark)
{
bookmark.CreatedAt = DateTime.UtcNow;
_context.Bookmarks.Add(bookmark);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetBookmark), new { id = bookmark.Id }, bookmark);
}
}
What does this code do?
The GetBookmark method handles GET /api/bookmarks/{id} requests. It looks up a single bookmark by ID and returns 404 Not Found if it doesn't exist.
The PostBookmark method saves the bookmark to the database with SaveChangesAsync(). It returns an HTTP 201 Created response with a Location header pointing to the new bookmark's URL. The nameof(GetBookmark) ensures the action name stays in sync if you rename the method later.
- Save Controllers/BookmarksController.cs.
Seeing red squiggles in VS Code?
Make sure you have the closing } brace for the class at the very end of the file. The file should start with the using statements and end with a single } closing the class.
Also confirm you added using Microsoft.EntityFrameworkCore; at the top. This import is needed for ToListAsync() and FindAsync().
Help me fix my controller errors.
✔️ Awesome, I've got everything!
Great. Make sure you saved the file, then let's test it.
ⓧ I'd like to double check the full code
Your Controllers/BookmarksController.cs should contain two chunks in order. Scroll up and confirm you have:
- The using statements, namespace, class declaration, _context field, constructor, and GetBookmarks method.
- The GetBookmark (by ID) and PostBookmark methods, followed by a closing } brace.
Time to prove the database actually works. Before you run the app, think about what you expect to happen. Last time, a POST followed by a GET returned an empty array. What do you think will happen now?
- Run the app by executing this command:
dotnet run --launch-profile https
- Once you see the URL in the terminal output, click the Send Request link above the POST request in your BookmarkApi.http file to create a bookmark.
- Now click Send Request above the GET request.
You should see your bookmark returned in a JSON array. The database persisted it across requests.
- Send the POST request again to create a second bookmark.
- Send the GET request again.
Both bookmarks now appear in the response. Your data survives between requests.
- Stop the app with Ctrl+C in the terminal.
Still getting an empty array from GET?
Make sure you saved both Program.cs and Controllers/BookmarksController.cs before running dotnet run. The app compiles from disk, not from unsaved editor buffers.
Check that your POST request in BookmarkApi.http returns a 201 status code (not 400 or 500). If you see an error status, the JSON body might be malformed.
Help me debug my empty GET response.
Your API can now store and retrieve data reliably. Next up, you'll add the remaining CRUD operations and a visual Swagger UI to test everything interactively.
Complete CRUD and Add Swagger UI
Your API can create bookmarks and list them all. A production-quality API also needs the ability to fetch a single item, update it, and remove it.
In this step, you'll add the remaining CRUD operations to your controller and install Swagger UI, a visual testing interface that lets you test every endpoint directly from your browser.
In this step, get ready to:
- Add GET-by-ID, PUT, and DELETE endpoints to your controller.
- Install Swagger UI for interactive API testing.
- Test all five operations through the Swagger interface.
Add a GET-by-ID endpoint
Your API can return every bookmark at once, but it has no way to fetch a single one by its ID. You also have a reference to GetBookmark in your PostBookmark method that doesn't resolve yet. Adding this method fixes both problems.
- In Controllers/BookmarksController.cs, find the closing } of the GetBookmarks method and add the following method below it:
[HttpGet("{id}")]
public async Task<ActionResult<Bookmark>> GetBookmark(long id)
{
var bookmark = await _context.Bookmarks.FindAsync(id);
if (bookmark == null)
{
return NotFound();
}
return bookmark;
}
What does this code do?
This method handles GET requests to /api/bookmarks/{id} where {id} is a number the caller provides in the URL.
- FindAsync(id) searches the database for a bookmark matching that ID.
- If no bookmark exists with that ID, the method returns a 404 Not Found response instead of crashing.
- If the bookmark exists, it returns it as JSON with a 200 OK status.
- Save Controllers/BookmarksController.cs.
- Run your app by executing this command in your terminal:
dotnet run --launch-profile https
- Use the BookmarkApi.http file to send a POST request (creating a bookmark).
- Open your browser and navigate to your app's HTTPS URL (shown in the terminal output) with /api/bookmarks/1 appended.
You should see the single bookmark you just created returned as JSON, with its id, title, url, and createdAt fields.
- Stop the app with Ctrl+C.
Seeing a 404 when you visit /api/bookmarks/1?
- Make sure you sent the POST request first. The in-memory database starts empty every time the app restarts.
- Check that the URL matches your actual port number from the terminal output, not 7001.
- help me debug my GET-by-ID endpoint returning 404
Add PUT and DELETE endpoints
With GET covered, your API still can't update or remove bookmarks. The PUT method lets callers change a bookmark's title or URL. The DELETE method removes a bookmark entirely.
- In Controllers/BookmarksController.cs, find the closing } of the PostBookmark method and add the following method below it:
[HttpPut("{id}")]
public async Task<IActionResult> PutBookmark(long id, Bookmark bookmark)
{
if (id != bookmark.Id)
{
return BadRequest();
}
_context.Entry(bookmark).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Bookmarks.Any(e => e.Id == id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
What does the PUT method do?
- The if (id != bookmark.Id) check ensures the URL's ID matches the bookmark data being sent. If they don't match, it returns 400 Bad Request.
- EntityState.Modified tells Entity Framework Core that every field on this bookmark has potentially changed and should be saved.
- The try/catch handles the case where another request deleted the bookmark between when you loaded it and when you saved. If the bookmark no longer exists, it returns 404 Not Found.
- On success, it returns 204 No Content because there's no data to send back.
- Below the PutBookmark method you just added, add the DELETE method:
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteBookmark(long id)
{
var bookmark = await _context.Bookmarks.FindAsync(id);
if (bookmark == null)
{
return NotFound();
}
_context.Bookmarks.Remove(bookmark);
await _context.SaveChangesAsync();
return NoContent();
}
What does the DELETE method do?
- It looks up the bookmark by ID using FindAsync. If no bookmark exists, it returns 404 Not Found.
- If the bookmark exists, Remove() marks it for deletion, and SaveChangesAsync() commits that change to the database.
- It returns 204 No Content on success because the resource no longer exists.
✔️ Awesome, I've got everything!
Great. Make sure you've saved Controllers/BookmarksController.cs with all three new methods in place.
ⓧ I'd like to double check the full code
Your Controllers/BookmarksController.cs should now contain these methods in order. Scroll up and confirm each is present:
- GetBookmarks (GET all, from the previous step).
- GetBookmark (GET by ID, added in this step).
- PostBookmark (POST, from the previous step).
- PutBookmark (PUT, added in this step).
- DeleteBookmark (DELETE, added in this step).
- Save Controllers/BookmarksController.cs.
- Verify the app still compiles by running:
dotnet run --launch-profile https
The app starts without errors. You should see the HTTPS URL in the terminal output. This confirms your new methods compile correctly.
- Stop the app with Ctrl+C.
Seeing a build error?
- Check that your PutBookmark method has both the long id and Bookmark bookmark parameters. Missing either one causes a signature error.
- Make sure every method's closing } brace is matched. A missing brace at the end of PutBookmark (which is the longest method) is the most common typo.
- help me fix my build error in BookmarksController
Install Swagger UI and test your API
Your five endpoints all work, but testing PUT and DELETE from a browser is awkward. Swagger UI gives you an interactive page where you can fill in parameters, send requests, and see responses for every endpoint.
- Install the NSwag package by running this command:
dotnet add package NSwag.AspNetCore --version 14.6.3
What is NSwag?
NSwag is a library that generates interactive API documentation from your code. It reads the OpenAPI spec that ASP.NET Core already produces and renders a web page where you can test endpoints directly.
- In Program.cs, find this block:
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
- Add the Swagger UI middleware directly below app.MapOpenApi(); so the block looks like this:
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwaggerUi(options =>
{
options.DocumentPath = "/openapi/v1.json";
});
}
What does this configuration do?
- app.UseSwaggerUi() registers the Swagger UI middleware that serves the interactive testing page.
- DocumentPath tells Swagger UI where to find the OpenAPI specification file. ASP.NET Core's built-in MapOpenApi() serves that file at /openapi/v1.json.
- Wrapping it inside the IsDevelopment() check means Swagger UI only appears in development mode, not in production.
✔️ Awesome, I've got everything!
Great. Make sure you've saved Program.cs with the new lines inside the IsDevelopment() block.
ⓧ I'd like to double check the full code
Your full Program.cs should look exactly like this:
using Microsoft.EntityFrameworkCore;
using BookmarkApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.Services.AddDbContext<BookmarkContext>(opt =>
opt.UseInMemoryDatabase("BookmarkStore"));
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwaggerUi(options =>
{
options.DocumentPath = "/openapi/v1.json";
});
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
- Save Program.cs.
Before you run the app, what do you think you'll see if you navigate to /swagger in your browser?
- Start your app by running:
dotnet run --launch-profile https
- Open your browser and navigate to your app's HTTPS URL (from the terminal output) with /swagger appended.
You should see the Swagger UI page with all five endpoints listed: GET all, GET by ID, POST, PUT, and DELETE. Each one has a Try it out button.
- Click Try it out on the POST endpoint.
- Replace the example JSON body with a bookmark title and URL, then click Execute.
- Click Try it out on the GET (all) endpoint and click Execute. You should see your bookmark in the response.
- Test PUT by entering the bookmark's ID and updating its title. Click Execute and confirm you get a 204 response.
- Test DELETE by entering the same ID and clicking Execute. Confirm you get a 204 response.
- Call GET all one more time. The response should be an empty array, confirming the delete worked.
- Stop the app with Ctrl+C.
Don't see the Swagger page?
- Make sure the URL ends with /swagger (not /swagger/index.html).
- Confirm you added app.UseSwaggerUi(...) inside the if (app.Environment.IsDevelopment()) block, not outside it.
- Check that dotnet add package NSwag.AspNetCore --version 14.6.3 completed without errors. If it failed, re-run it.
- help me get Swagger UI working in my ASP.NET Core project
You now have a complete REST API with full CRUD operations and interactive documentation. Next up, you'll take on a Secret Mission to add a search feature that filters bookmarks by keyword.
Secret mission
Add a Search Endpoint
Your API returns all bookmarks or one by ID. But what if you have hundreds of bookmarks and need to find one by keyword? Add a search endpoint that filters bookmarks by title, teaching you query parameters and LINQ filtering.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with an in-memory database, so there are no ongoing costs.
Resources you used:
- BookmarkApi project folder on your Desktop.
- .NET 10 SDK (installed via winget).
- C# Dev Kit extension in VS Code.
Keep everything running
No action needed. Choose this if you plan to keep building on this project or start another .NET project soon.
- Your BookmarkApi folder stays on your Desktop, ready for you to reopen in VS Code.
- The .NET 10 SDK remains installed and available for future projects.
- The C# Dev Kit extension stays active in VS Code.
- Since the app uses an in-memory database, all bookmark data is cleared every time you stop the app. No data lingers between sessions.
Pause - I'll come back to this later
Since the app is already stopped, there is nothing actively running. Your project files stay on disk so you can pick up where you left off.
- Confirm the app is stopped. If you still have a terminal running dotnet run, press Ctrl+C to stop it.
- Close VS Code to free up memory.
- When you return, open the BookmarkApi folder in VS Code and run dotnet run --launch-profile https to start the API again.
Delete - I don't want to use this again
Remove all project files and optionally uninstall the tools you set up for this project.
- Delete the project folder by running this command in PowerShell:
Remove-Item -Recurse -Force ~\Desktop\BookmarkApi
- Uninstall the .NET 10 SDK by running:
winget uninstall Microsoft.DotNet.SDK.10
- Remove the C# Dev Kit extension from VS Code. Open VS Code, press Ctrl+Shift+X to open the Extensions panel, find C# Dev Kit, and click Uninstall.
Nice Work!
Nice Work!
You just built a fully functional REST API from scratch using ASP.NET Core with .NET 10. You can create, read, update, and delete bookmarks through HTTP requests, and test every endpoint in an interactive browser UI.
You've learned how to:
- Build a REST API with controllers, models, and route attributes that handle GET, POST, PUT, and DELETE requests.
- Use Entity Framework Core with dependency injection to give your controller a shared database that persists data across requests.
- Add Swagger UI for interactive API documentation so you can test every endpoint directly in the browser.
- Secret Mission: Added a search endpoint using LINQ filtering and query parameters to find bookmarks by title keyword.
Ready to quiz yourself?