Tutorials / Build a REST API with C# and .NET · Lesson 16 of 17
On this page
The First Endpoint and Swagger
Last lesson we created the project and ran the sample /weatherforecast endpoint. It has nothing to do with books.
Our goal in this lesson is small: serve one thing over REST. One endpoint that returns a list of three books. It is a first taste of building an API, so you see the whole path once before we add more.
Here is the route:
- A short review of REST, and how a request turns into code.
- We write the endpoint the simplest way, called a minimal API, and run it.
- We add a page that lets us test it easily while we build.
- We rewrite the same endpoint as a controller. Most existing .NET projects organise their endpoints that way, so every lesson after this one uses controllers.
The list of books is typed straight into the code. There is no database yet (lesson 16) and no service class yet (lesson 17).
Where we are
This is the solution from lesson 14. Open it in your editor or Visual Studio, because we change a few files in it. The files we touch in this lesson are marked:
BookShop/
BookShop.slnx
Directory.Build.props
Directory.Packages.props <- changes (adds a package version)
src/
BookShop.Api/
BookShop.Api.csproj <- changes (adds a package)
Program.cs <- changes (our endpoint, then the setup)
Book.cs <- NEW
Controllers/ <- NEW (at the end of the lesson)
BooksController.cs <- NEW
BookShop.Api.http <- changes (optional)
Every change in this lesson is inside src/BookShop.Api/, except the version line in Directory.Packages.props. The commands in this lesson are run from the solution folder, BookShop/, the one that holds BookShop.slnx.
From REST to code
In lesson 00 you learned the pattern of REST: a noun in the URL, a verb in the method. A client sends a request such as GET /books. That means: "show me the books".
Now we are on the other side. We are the server, and we write the code that answers. The code must say three things:
- Which verb it answers:
GET. - Which address it answers:
/books. - What to do: find the books and send them back.
An endpoint is exactly that: one verb, one address and the code that runs for them. Here is the line the template gave us last lesson, reduced to its shape:
app.MapGet("/weatherforecast", () =>
{
// the template builds five fake forecasts here
});
MapGetis the verb. There is oneMap...method for each HTTP verb. The table below lists them.- The address is the path after the server address.
"/books"answershttp://localhost:5155/books. - The code is a lambda: a small function with no name.
()is its list of inputs (empty here),=>means "gives", and what follows is what it does. Whatever it returns becomes the response.
Each verb has its own method. This is the map of the whole course, so you know what is coming:
| HTTP verb | Minimal API (first half) | Controller (second half) | A bookshop example | Lesson |
|---|---|---|---|---|
GET |
MapGet |
[HttpGet] |
list the books, or get one book | 15 and 18 |
POST |
MapPost |
[HttpPost] |
add a new book | 20 |
PUT |
MapPut |
[HttpPut] |
replace a whole book | 21 |
PATCH |
MapPatch |
[HttpPatch] |
change one part of a book, such as the price | 21 |
DELETE |
MapDelete |
[HttpDelete] |
remove a book | 21 |
(PUT and PATCH differ in how much they change: lesson 00 explains it.) Today we only need the first row.
Write the books endpoint
Two files change: a new file for the Book, and Program.cs for the endpoint.
Step 1: create Book.cs
A book needs a place in the code. We give it its own file, next to Program.cs, in the src/BookShop.Api/ folder. One idea per file keeps a project easy to read.
Terminal: create a file named Book.cs in src/BookShop.Api/ with your text editor.
Visual Studio: right-click the BookShop.Api project in Solution Explorer, choose Add, then Class..., and name it Book.cs. Visual Studio fills the file with a class and a namespace. Delete everything in it.
Either way, the whole file is this:
public class Book
{
public int Id { get; set; }
public string Title { get; set; } = "";
public string Author { get; set; } = "";
public decimal Price { get; set; }
}
It is the same kind of class as in lesson 08: a class that only holds data, with one property for each fact about a book.
decimalis the C# type for money. Themwe write after a number later says "this is a decimal".= ""gives the text properties an empty text to start with. Without it, the compiler warns that aTitlemight be missing (this comes from theNullablesetting inDirectory.Build.props, lesson 14).- There is no
namespaceline, on purpose. Without one, the other files can useBookstraight away. In lesson 16 the class moves to a proper project of its own.
Step 2: change Program.cs
Open src/BookShop.Api/Program.cs. Delete the whole /weatherforecast endpoint, and the summaries list and the WeatherForecast type at the bottom that the template put in the file. We do not need them.
After the change, the whole file is this. Compare it with yours, and use it if you get lost:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.MapGet("/books", () =>
{
var books = new List<Book>
{
new Book { Id = 1, Title = "The Pragmatic Programmer", Author = "Andrew Hunt", Price = 39.90m },
new Book { Id = 2, Title = "Clean Code", Author = "Robert C. Martin", Price = 34.50m },
new Book { Id = 3, Title = "Refactoring", Author = "Martin Fowler", Price = 44.00m }
};
return books;
})
.WithName("GetBooks");
app.Run();
The MapGet stands where the old endpoint stood, after UseHttpsRedirection() and before Run(). .WithName("GetBooks") gives the endpoint a name that tools reading your API show.
new List<Book> { ... } makes a list of books and puts the three books in it in one go. Each new Book { ... } makes one book and sets its properties, as in lesson 08.
We never wrote code to turn the list into JSON. ASP.NET does it for us. It sees that the lambda returned a list of Book, writes JSON (lesson 08), and adds the status code 200 OK (lesson 00).
Run it
From the solution folder, as in lesson 14:
dotnet run --project src/BookShop.Api
In Visual Studio, press Ctrl+F5 instead, as in lesson 14. Use the address that the terminal (or the console window Visual Studio opens) prints (ours was http://localhost:5155, yours can differ). Open /books on it in the browser. You will see the list of books as JSON.
A browser shows only the body of the response. To see the whole response, with the status code and the headers, we use a small tool called curl. Think of curl as a browser without the window. It sends the same kind of request a browser does, but you type it in a terminal, and it prints the answer there as plain text.
That has two benefits for us:
- Text is easy to compare. This guide can show you exactly what curl prints, and you can put it next to your own screen. A picture of a browser would not tell you much more than the JSON.
- Developers use curl all the time. It works on servers that have no browser, it goes into scripts, and people paste curl commands into bug reports so others can repeat the problem. It is worth knowing.
curl is already installed on Windows 10 and 11, macOS and Linux. We only need it for a few minutes in this lesson, because soon we add a page that does the same with buttons.
The terminal that runs the app is busy, so open a second terminal:
- Windows: press the Windows key, type
cmd, and press Enter. This opens Command Prompt. (Windows Terminal and PowerShell work too.) - macOS: press Cmd+Space, type
Terminal, and press Enter. - Linux: open your Terminal app. On many systems Ctrl+Alt+T does it.
- Visual Studio: the menu View, then Terminal, opens one inside the window.
Then type:
curl -i http://localhost:5155/books
-i means "also print the response headers". Without it, curl prints only the body.
⚠️ In the older Windows PowerShell,
curlis a different command, and you get a confusing error. Typecurl.exeinstead (curl.exe -i http://localhost:5155/books). In Command Prompt, macOS and Linux, plaincurlworks.
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Wed, 21 Oct 2026 07:34:05 GMT
Server: Kestrel
Transfer-Encoding: chunked
[{"id":1,"title":"The Pragmatic Programmer","author":"Andrew Hunt","price":39.90},{"id":2,"title":"Clean Code","author":"Robert C. Martin","price":34.50},{"id":3,"title":"Refactoring","author":"Martin Fowler","price":44.00}]
(The date will differ.) That is a real response: status 200 OK, a header that says the body is JSON, and the three books. Notice that the property names came out in lower case (title, not Title). ASP.NET follows the usual JSON habit for you.
Stop the app with Ctrl+C when you are done.
The problem: how do we test it?
The browser address bar just worked, so why do we need anything else?
Because the address bar can only do one thing: send a GET. Later we build endpoints that create, change and delete books. Those need POST, PUT and DELETE, and a body of JSON to send. Try a POST to our endpoint and see what happens. -X POST tells curl which verb to use. Without it, curl sends a GET:
curl -i -X POST http://localhost:5155/books
HTTP/1.1 405 Method Not Allowed
405 Method Not Allowed is correct: /books only answers GET. But it shows the point. curl can send any verb, but you must type every request, and later requests will need a JSON body too. We want a tool that is easier than that.
Every API needs an easy way to be tested by hand while you build it. The good news: the API can make one for itself. It is a web page that lists every endpoint and has a button to try each one.
People call this page by different names. You will hear Swagger, Swagger UI, Scalar or "API explorer". The names belong to different tools, but they do the same job. In this course we say Swagger for the idea, because that is the word you will hear most. The tool we install is Scalar. If you join a project that uses Swagger UI, you will find the same page with a different look.
How the test page knows your endpoints
The page does not read your C# code. It reads a description of your API.
You met the part that makes it in lesson 14, in the tour of Program.cs. AddOpenApi() and MapOpenApi() are already in your file. They make a JSON document, in a standard format called OpenAPI, that lists every endpoint: its route, its verb and the shape of its response. Run the app and open /openapi/v1.json on it to see the document. The start of it looks like this:
{
"openapi": "3.1.1",
"info": { "title": "BookShop.Api | v1", "version": "1.0.0" },
"paths": {
"/books": {
"get": {
"operationId": "GetBooks",
...
You can see our /books route, the get verb and the name GetBooks. (We cut the rest, and the real file is longer.)
The test page is a second piece that reads this document and draws a nice page from it. The template did not add that piece, so we add it now.
Add Scalar
Terminal
From the solution folder:
dotnet add src/BookShop.Api package Scalar.AspNetCore
Remember lesson 14: the project file gets a PackageReference with no version, and the version goes into Directory.Packages.props. Open that file and you will find a new line next to the OpenApi one. On the day we wrote this lesson it was:
<PackageVersion Include="Scalar.AspNetCore" Version="2.18.2" />
Your version number may be newer. That is fine.
Visual Studio
- In Solution Explorer, right-click the
BookShop.Apiproject and choose Manage NuGet Packages. - Open the Browse tab and search for
Scalar.AspNetCore. - Select it and click Install.
Both routes give the same result.
Switch it on
Two small changes in Program.cs. Add a using line at the very top, and one line inside the development block. The two new lines are marked. The whole file now looks like this:
using Scalar.AspNetCore; // new
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference(); // new
}
app.UseHttpsRedirection();
app.MapGet("/books", () =>
{
var books = new List<Book>
{
new Book { Id = 1, Title = "The Pragmatic Programmer", Author = "Andrew Hunt", Price = 39.90m },
new Book { Id = 2, Title = "Clean Code", Author = "Robert C. Martin", Price = 34.50m },
new Book { Id = 3, Title = "Refactoring", Author = "Martin Fowler", Price = 44.00m }
};
return books;
})
.WithName("GetBooks");
app.Run();
MapScalarApiReference() is another endpoint (lesson 14: the test page is served by your app too). It sits in the development block next to MapOpenApi(), so the test page exists on your own machine only. A live server does not offer it.
Try it
Run the app again, and open /scalar on its address, for example http://localhost:5155/scalar.
You will see your API on the page. The left menu lists your endpoint: GET /books. Click it.
- The page shows the request it would send, and the shape of the answer.
- Click Test Request and send the request.
- The answer appears: the status
200and the list of books.
You just tested your endpoint without writing any test code and without leaving the browser. This is the quick check from lesson 12: the backend developer writes an endpoint, then looks at it here to see that it does what was agreed.
Try one more thing. Add a fourth book to the list in Program.cs, run the app again and send the request again. The page needs no change. It reads the document, so it always matches your code.
💡 The template also made a file called
BookShop.Api.http(lesson 14). It is a plain text file of requests that many editors can send with a click. Its advantage is that the requests are saved in the project, so a team can share them. Its disadvantage is that you write each request by hand, while the test page draws them for you. We use the test page in this course. If you prefer the file, change its request toGET {{BookShop.Api_HostAddress}}/booksand use the port your machine shows.
The same endpoint as a controller
Our endpoint works, and it is easy to read, because everything is in one file. Think about what happens next. Every lesson adds endpoints: get one book, add a book, change it, delete it, a report. If they all go into Program.cs, the file grows long and mixes three jobs: setting up the app, listing the routes, and holding the logic. A real project does not keep it like that.
The usual fix is a controller: a class that holds all the endpoints for one thing. A BooksController holds everything about books. An AuthorsController would hold everything about authors. Each controller lives in its own file, so you always know where to look.
The behaviour does not change. We only move the endpoint to a better place.
Step 1: create BooksController.cs
Controllers go in a folder named Controllers, inside src/BookShop.Api/.
Terminal: create the folder, then create a file named BooksController.cs in it with your text editor:
mkdir src/BookShop.Api/Controllers
Visual Studio: right-click the BookShop.Api project, choose Add, then New Folder, and name it Controllers. Right-click the new folder, choose Add, then Class..., and name it BooksController.cs. Delete everything Visual Studio puts in the file.
Either way, the whole file is this:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("books")]
public class BooksController : ControllerBase
{
[HttpGet]
public List<Book> GetAll()
{
var books = new List<Book>
{
new Book { Id = 1, Title = "The Pragmatic Programmer", Author = "Andrew Hunt", Price = 39.90m },
new Book { Id = 2, Title = "Clean Code", Author = "Robert C. Martin", Price = 34.50m },
new Book { Id = 3, Title = "Refactoring", Author = "Martin Fowler", Price = 44.00m }
};
return books;
}
}
The three parts of the endpoint are all still here, in a different form:
[HttpGet]says this method answers GET requests. It is the controller's version ofMapGet. A marker in square brackets above a class or method is called an attribute.[Route("books")]sits on the class, and gives the address for every method in it./books, like before. When we add more methods later, they share this address, and you write it once.GetAll()is a normal method. The name is ours, and it only helps us read the code. What it returns becomes the response, exactly as the lambda did.public class BooksControlleris the class that holds them. By habit the name ends inController.: ControllerBasemeans the class builds on a ready-made class from ASP.NET. It gives us helper methods that we start using in lesson 18, such as the ones that return a 404.[ApiController]tells ASP.NET that this class is the controller of an API. It switches on some helpful behaviour that we meet in lesson 20.using Microsoft.AspNetCore.Mvc;is the line that lets the file use those names. They live in that namespace.
Step 2: change Program.cs
Two things change. The controller feature must be switched on, and the old MapGet must stop answering, because the controller now answers /books.
We comment the old endpoint out instead of deleting it, so you can see what moved. A comment is a line that starts with //. C# ignores it. The whole file is now:
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); // new
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
app.MapControllers(); // new
// The minimal API endpoint, now moved into BooksController:
// app.MapGet("/books", () =>
// {
// var books = new List<Book>
// {
// new Book { Id = 1, Title = "The Pragmatic Programmer", Author = "Andrew Hunt", Price = 39.90m },
// new Book { Id = 2, Title = "Clean Code", Author = "Robert C. Martin", Price = 34.50m },
// new Book { Id = 3, Title = "Refactoring", Author = "Martin Fowler", Price = 44.00m }
// };
// return books;
// })
// .WithName("GetBooks");
app.Run();
Compare the commented lines with the GetAll method in the controller. The list of books moved there unchanged. Once you have seen it, delete the comment block. It does nothing, and later lessons show Program.cs without it.
Look at what is left in the file. It is short, and it will stay short. It only sets up the app.
AddControllers()is in the services part (the second step in the four-step picture from lesson 14). It registers what the app needs to work with controllers.MapControllers()is in the endpoints part. It finds every controller in the project and makes their methods endpoints. One line, however many controllers we add.
Run it again
Run the app and ask for the books:
curl -i http://localhost:5155/books
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Wed, 21 Oct 2026 08:40:32 GMT
Server: Kestrel
Transfer-Encoding: chunked
[{"id":1,"title":"The Pragmatic Programmer","author":"Andrew Hunt","price":39.90},{"id":2,"title":"Clean Code","author":"Robert C. Martin","price":34.50},{"id":3,"title":"Refactoring","author":"Martin Fowler","price":44.00}]
It is the same answer as before. Now open /scalar. The page lists the same GET /books. The only visible change is the group name in the left menu: it now says Books, taken from the name of the controller, where before it said BookShop.Api. Remember the OpenAPI document: the page reads a description of the endpoints, and it does not care how you wrote them.
That is the proof that we only changed the shape of the code, not what it does.
From here on: controllers
From now on, every endpoint in this course is a controller method. There are two reasons:
- Most existing .NET projects organise their endpoints this way. If you open a project at work, a controllers folder is what you will most likely find.
- It gives every piece a place. A controller receives the request. Later we add a service that does the work, and a data layer that stores it. Each lives in its own file, and that is how a project stays easy to read as it grows.
The minimal API way is real too. It is fully supported by .NET, some teams use it, and the ideas in this course (status codes, validation, errors, tokens, tests) are the same in both. We will not teach it in this course, so that we can focus on one way and go deeper. If you are curious, learning minimal APIs is a good exercise to do on your own after the course, using Microsoft's documentation. You already know what its code looks like: you wrote it in the first half of this lesson.
Recap
This is where everything is now:
BookShop/
BookShop.slnx
Directory.Build.props
Directory.Packages.props Scalar version added
src/
BookShop.Api/
BookShop.Api.csproj Scalar package added
Program.cs setup only: controllers, OpenAPI, Scalar
Book.cs the Book class
Controllers/
BooksController.cs the GET /books endpoint
BookShop.Api.http unused sample requests
- An endpoint is one verb, one address, and the code that answers. The verb and address come from the request (
GET /books, lesson 00). The code builds the response. - In a minimal API,
MapGet("/books", ...)writes all three in one line. The lambda is a small function with no name, and what it returns becomes the response. A returned list becomes JSON with status200, and ASP.NET does that for you. - The browser address bar can only send GET, and a request with the wrong verb gets
405 Method Not Allowed. To test every verb, you need a test tool. - Swagger is our word for the page an API makes for itself so you can try its endpoints. Other names for it are Swagger UI, Scalar and API explorer.
- The page reads the OpenAPI document from
/openapi/v1.json, so it always matches your code. We installed Scalar withdotnet add package, switched it on withMapScalarApiReference()inside the development block, and opened/scalar. - A controller is a class that holds the endpoints for one thing.
[Route]gives the address,[HttpGet]gives the verb, and a method is the code.AddControllers()andMapControllers()switch controllers on inProgram.cs. - Moving the endpoint into a controller changed the shape of the code, not the behaviour. From here on, the course uses controllers.
Questions to think about
The answers are not given here on purpose, so check yourself against the lesson.
- A request arrives as
GET /books. Which part of it is the verb and which is the address? Where do you write each of them inMapGet, and where in a controller? - You send
POST /booksand get405. Does this mean the server is broken? Explain. - What does the test page read to know which endpoints exist? What happens on the page when you add a new endpoint?
- A colleague says "open Swagger" and you have Scalar in your project. Is something wrong?
- We moved the endpoint from
Program.csintoBooksController. What stayed the same, and what changed? Name one thing you checked to be sure.
Next lesson: the books must come from somewhere real, so we make a data project and a database for them.