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:

    1. A short review of REST, and how a request turns into code.
    2. We write the endpoint the simplest way, called a minimal API, and run it.
    3. We add a page that lets us test it easily while we build.
    4. 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:

    1. Which verb it answers: GET.
    2. Which address it answers: /books.
    3. 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
    });
    
    From a request to the lines of an endpoint The request GET /books send back the books the verb the address what to do MapGet "/books" () => { ... } The endpoint in code
    • MapGet is the verb. There is one Map... method for each HTTP verb. The table below lists them.
    • The address is the path after the server address. "/books" answers http://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.

    • decimal is the C# type for money. The m we 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 a Title might be missing (this comes from the Nullable setting in Directory.Build.props, lesson 14).
    • There is no namespace line, on purpose. Without one, the other files can use Book straight 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).

    The path of a request to /books Request GET /books Route is it /books? Lambda returns the list Response 200 + JSON ASP.NET turns the list into JSON

    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, curl is a different command, and you get a confusing error. Type curl.exe instead (curl.exe -i http://localhost:5155/books). In Command Prompt, macOS and Linux, plain curl works.

    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.

    The address bar against a test page Browser address bar GET only one verb A test page GET POST PUT DELETE every verb, with a body

    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.

    From endpoints to a test page Your endpoints in your code OpenAPI a JSON document Test page reads the document add an endpoint and the page updates by itself

    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

    1. In Solution Explorer, right-click the BookShop.Api project and choose Manage NuGet Packages.
    2. Open the Browse tab and search for Scalar.AspNetCore.
    3. 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.

    1. The page shows the request it would send, and the shape of the answer.
    2. Click Test Request and send the request.
    3. The answer appears: the status 200 and 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 to GET {{BookShop.Api_HostAddress}}/books and 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:

    The minimal API and the controller, part by part Minimal API in Program.cs Controller in BooksController.cs MapGet [HttpGet] "/books" [Route("books")] () => { ... } GetAll() { ... } same verb, same address, same code
    • [HttpGet] says this method answers GET requests. It is the controller's version of MapGet. 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 BooksController is the class that holds them. By habit the name ends in Controller.
    • : ControllerBase means 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 status 200, 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 with dotnet add package, switched it on with MapScalarApiReference() 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() and MapControllers() switch controllers on in Program.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.

    1. 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 in MapGet, and where in a controller?
    2. You send POST /books and get 405. Does this mean the server is broken? Explain.
    3. What does the test page read to know which endpoints exist? What happens on the page when you add a new endpoint?
    4. A colleague says "open Swagger" and you have Scalar in your project. Is something wrong?
    5. We moved the endpoint from Program.cs into BooksController. 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.

    ← Create the Solution and the API ProjectA Data Project and the First Table with EF Core →