DEV Community

Cover image for Documenting Your .NET Web API with Swagger: The Fun Guide
Zied Rebhi
Zied Rebhi

Posted on

Documenting Your .NET Web API with Swagger: The Fun Guide

magine this: You’re the new dev on the team, and someone hands you the API code to work with. You open it up and stare at it, confused, like you’ve just found a treasure map with no X marks the spot. You’re thinking, Whaaaat is this thing even supposed to do? 🧐

Or picture this: You’re the QA tester, and you’ve been handed this mysterious API with zero documentation. Every time you try to test, you end up with more questions than answers. You find yourself emailing the dev team with questions like, What does this endpoint even return? How do I use this thing? 😵

Or, worst case scenario, you’re another team that’s supposed to use the API you’ve never seen before. You try making a call, and then… boom! You’re calling the original devs non-stop for clarification, like, Hey, how do I get a list of food items? What’s the format of the response? It’s like they’re your personal helpdesk. 📞💻

We’ve all been there, right? But fear not, because Swagger is here to save the day! 🦸‍♂️ Let’s show you how you can turn your messy, undocumented API into something that even your new teammate, your tester, and those poor souls in other teams can understand with zero hassle.

Full documented API

Why Document Your API?

So why should we go through all this effort? Well, imagine trying to navigate a new city without a map — that’s what it’s like trying to use an undocumented API. Documentation makes everything clearer, and it’s not just for you! It’s for your team, for testers, and for those poor souls in other teams who just want to use your API without calling you every hour. 🤳

Step 1: Set Up Swagger

First things first: let’s set up Swagger in your project. Swagger turns your API into a shiny, interactive dashboard. It’s like giving your API a GPS, a guidebook, and a snack (just to make it extra pleasant). 🗺️🍿

builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Version = "v1",
        Title = "Food API",
        Description = "A sample application with Swagger, Swashbuckle, and API versioning for managing Foods",
        TermsOfService = new Uri("https://example.com/terms"),
        Contact = new OpenApiContact
        {
            Name = "Example Contact",
            Url = new Uri("https://example.com/contact")
        },
        License = new OpenApiLicense
        {
            Name = "Example License",
            Url = new Uri("https://example.com/license")
        },
        TermsOfService = new Uri("https://example.com/terms"),
    });
 // using System.Reflection;
 var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
 var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
 options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true);
});
Enter fullscreen mode Exit fullscreen mode

Then enable XML comments by adding this to your _.csproj _file :

Right-click the project in Solution Explorer and select Edit .csproj.

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
Enter fullscreen mode Exit fullscreen mode

Now you’re ready to start adding documentation to your API. This is where the magic happens!

Step 2: Documenting Your API with Swagger Annotations

Now let’s give your API some personality. Here’s an example of a Food API that’s fully documented. We’re going to make it clear, concise, and helpful — so no one has to call you 15 times a day asking questions. 😅

using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

namespace FoodApi.Controllers.v3
{
    /// <summary>
    /// Manages food-related operations.
    /// </summary>
    [ApiController]
    [ApiVersion("3.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class FoodController : ControllerBase
    {
        /// <summary>
        /// Retrieves a list of all food items.
        /// </summary>
        /// <remarks>
        /// Example request:
        /// `http
        /// GET /api/v3.0/Food
        /// `
        /// Example response:
        /// `json
        /// [
        ///     { "id": 1, "name": "Apple", "calories": 95 },
        ///     { "id": 2, "name": "Banana", "calories": 105 }
        /// ]
        /// `
        /// </remarks>
        [HttpGet]
        [SwaggerOperation(Summary = "Get all food items", Description = "Retrieves a list of all food items.")]
        [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<FoodItemDto>))]
        public ActionResult<IEnumerable<FoodItemDto>> GetAllFoods()
        {
            return Ok(new List<FoodItemDto>
        {
            new FoodItemDto { Id = 1, Name = "Apple", Calories = 95 },
            new FoodItemDto { Id = 2, Name = "Banana", Calories = 105 }
        });
        }

        /// <summary>
        /// Retrieves a specific food item by ID.
        /// </summary>
        /// <remarks>
        /// Example request:
        /// `http
        /// GET /api/v3.0/Food/1
        /// `
        /// Example response:
        /// `json
        /// { "id": 1, "name": "Apple", "calories": 95 }
        /// `
        /// </remarks>
        /// <param name="id">The unique identifier of the food item.</param>
        /// <response code="200">The food item was successfully retrieved.</response>
        /// <response code="404">The food item with the given ID was not found.</response>
        [HttpGet("{id}")]
        [SwaggerOperation(Summary = "Get a food item by ID", Description = "Retrieves a specific food item by its unique ID.")]
        [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FoodItemDto))]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public ActionResult<FoodItemDto> GetFoodById(int id)
        {
            if (id <= 0) return NotFound();

            return Ok(new FoodItemDto { Id = id, Name = "Apple", Calories = 95 });
        }

        /// <summary>
        /// Retrieves food details (deprecated method).
        /// </summary>
        /// <remarks>
        /// **This method is obsolete. Use `GetFoodById` instead.**
        /// Example usage:
        /// `http
        /// GET /api/v3.0/Food/details/1
        /// `
        /// **Response:**
        /// `json
        /// { "id": 1, "name": "Apple", "calories": 95 }
        /// `
        /// **Error Response:**
        /// `json
        /// { "error": "Not Found" }
        /// `
        /// </remarks>
        /// <param name="id">The unique identifier of the food item.</param>
        /// <response code="200">The food item was successfully retrieved.</response>
        /// <response code="404">The food item with the given ID was not found.</response>
        [Obsolete("This method is deprecated. Use GetFoodById instead.")]
        [HttpGet("details/{id}")]
        [SwaggerOperation(Summary = "Get food details (deprecated)", Description = "Retrieves detailed food information (use `GetFoodById` instead).")]
        [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FoodItemDto))]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public ActionResult<FoodItemDto> GetFoodDetails(int id)
        {
            if (id <= 0) return NotFound();

            return Ok(new FoodItemDto { Id = id, Name = "Apple", Calories = 95 });
        }

        /// <summary>
        /// Creates a new food item.
        /// </summary>
        /// <remarks>
        /// Example request:
        /// `json
        /// { "name": "Apple", "calories": 95 }
        /// `
        /// Example response:
        /// `json
        /// { "id": 123, "name": "Apple", "calories": 95 }
        /// `
        /// </remarks>
        /// <param name="foodItem">The food item to create.</param>
        /// <response code="201">The food item was successfully created.</response>
        /// <response code="400">Invalid food item details.</response>
        [HttpPost]
        [SwaggerOperation(Summary = "Create a new food item", Description = "Creates a new food item in the system.")]
        [ProducesResponseType(StatusCodes.Status201Created, Type = typeof(FoodItemDto))]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        public ActionResult<FoodItemDto> CreateFood(FoodItemDto foodItem)
        {
            if (string.IsNullOrEmpty(foodItem.Name) || foodItem.Calories <= 0)
            {
                return BadRequest("Invalid food item details.");
            }

            foodItem.Id = new Random().Next(1, 1000); // Simulate ID generation
            return CreatedAtAction(nameof(GetFoodById), new { id = foodItem.Id }, foodItem);
        }

        /// <summary>
        /// Updates an existing food item.
        /// </summary>
        /// <remarks>
        /// Example request:
        /// `json
        /// { "id": 1, "name": "Banana", "calories": 110 }
        /// `
        /// Example response:
        /// `json
        /// { "id": 1, "name": "Banana", "calories": 110 }
        /// `
        /// </remarks>
        /// <param name="id">The unique identifier of the food item to update.</param>
        /// <param name="foodItem">The updated food item data.</param>
        /// <response code="200">The food item was successfully updated.</response>
        /// <response code="400">Food item ID mismatch or invalid data.</response>
        /// <response code="404">The food item with the given ID was not found.</response>
        [HttpPut("{id}")]
        [SwaggerOperation(Summary = "Update an existing food item", Description = "Updates a specific food item.")]
        [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FoodItemDto))]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public ActionResult<FoodItemDto> UpdateFood(int id, FoodItemDto foodItem)
        {
            if (id != foodItem.Id) return BadRequest("Food item ID mismatch.");
            if (id <= 0) return NotFound();

            return Ok(foodItem); // Return the updated food item
        }

        /// <summary>
        /// Deletes a food item by ID.
        /// </summary>
        /// <remarks>
        /// Example request:
        /// `http
        /// DELETE /api/v3.0/Food/1
        /// `
        /// Example response:
        /// `json
        /// { "message": "Food item deleted successfully." }
        /// `
        /// </remarks>
        /// <param name="id">The unique identifier of the food item to delete.</param>
        /// <response code="200">The food item was successfully deleted.</response>
        /// <response code="404">The food item with the given ID was not found.</response>
        [HttpDelete("{id}")]
        [SwaggerOperation(Summary = "Delete a food item", Description = "Deletes a specific food item.")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public IActionResult DeleteFood(int id)
        {
            if (id <= 0) return NotFound();

            return Ok(new { Message = "Food item deleted successfully." });
        }

        /// <summary>
        /// Partially updates a food item (e.g., updating only calories).
        /// </summary>
        /// <remarks>
        /// Example request:
        /// `json
        /// { "calories": 110 }
        /// `
        /// Example response:
        /// `json
        /// { "id": 1, "name": "Apple", "calories": 110 }
        /// `
        /// </remarks>
        /// <param name="id">The unique identifier of the food item to update.</param>
        /// <param name="patchDoc">The JSON patch document containing the changes to apply.</param>
        /// <response code="200">The food item was successfully updated.</response>
        /// <response code="400">Invalid patch data.</response>
        /// <response code="404">The food item with the given ID was not found.</response>
        [HttpPatch("{id}")]
        [SwaggerOperation(Summary = "Partially update a food item", Description = "Partially updates an existing food item.")]
        [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FoodItemDto))]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public ActionResult<FoodItemDto> PatchFood(int id, [FromBody] JsonPatchDocument<FoodItemDto> patchDoc)
        {
            if (id <= 0) return NotFound();
            if (patchDoc == null) return BadRequest("Invalid patch data.");

            var foodItem = new FoodItemDto { Id = id, Name = "Apple", Calories = 95 };

            patchDoc.ApplyTo(foodItem, ModelState);

            if (!ModelState.IsValid) return BadRequest(ModelState);

            return Ok(foodItem);
        }
    }

    /// <summary>
    /// Represents a food item.
    /// </summary>
    public class FoodItemDto
    {
        /// <summary>
        /// Unique identifier of the food item.
        /// </summary>
        /// <example>1</example>
        public int Id { get; set; }

        /// <summary>
        /// The name of the food item.
        /// </summary>
        /// <example>Apple</example>
        public string Name { get; set; }

        /// <summary>
        /// The number of calories in the food item.
        /// </summary>
        /// <example>95</example>
        public int Calories { get; set; }
    }
}

Enter fullscreen mode Exit fullscreen mode

What Have We Done?

Let’s break down exactly what’s happening here. Here’s why this documentation is a game-changer:

1.Added Summaries:
Each method now has a /// tag, which is like giving your API a clear job description. For example, Retrieves a list of all food items is super helpful for anyone trying to understand what this endpoint does.

2. Clear Remarks:
We included /// to show exactly what’s expected when making requests and receiving responses. Now testers can see the request format, and other devs won’t be left in the dark wondering what a valid response looks like.

3. Error Handling:
We documented potential error responses with /// , which means if something goes wrong, your API will tell the user exactly what happened. No more guessing, no more surprises! (Remember, the only surprise should be when the food's really good. 😜)

4. Obsolete Methods:
We used the [Obsolete] attribute to let users know that old methods are no longer the way to go. This is a life-saver when the next dev or team calls you up and says, “Wait, should we use this?” Answer: “Nope, use the new one!” ✅

5. Response Types:
With the [ProducesResponseType] attribute, we made sure Swagger knows what kind of responses your API sends back. So now, your users (and that other dev team calling you) can easily see that a 200 OK means success, or 404 Not Found means they might want to double-check their inputs.

6. Parameter Descriptions:
We used /// to describe the parameters. Now, there’s no more “Wait, what does this ID mean?” Everyone knows exactly what each parameter represents. It’s like providing a map for your API. 🗺️

7. Add more descriptions to our objects / DTOs :

    /// <summary>
    /// The name of the food item.
    /// </summary>
    /// <example>Apple</example>
    public string Name { get; set; }
Enter fullscreen mode Exit fullscreen mode

And That’s It!

Now your API is fully documented, and everyone (even the new devs, testers, and other teams) will love using it. No more confusing, undocumented endpoints. No more “Hey, how do I use this thing?” phone calls. Swagger makes your API transparent, easy to understand, and interactive. 🎉

So, to recap:

  • No more confusion for the new devs.

  • No more endless questions from testers.

  • No more panic calls from other teams wondering how to use your API.

Your API is now a smooth, well-documented experience for everyone involved. 🚀

Top comments (0)