This blog explains how to create REST APIs in ASP .NET Core.
New project
Open up Visual Studio, choose Create new project, and search for Web API:
On the next screen, add the project name and click the continue button until the process is finished.
Setting up the Web API controller
In the newly created project, focus on the Controllers folder. This is where REST endpoints live.
Controller Class
Create a new empty class C#
public class TodosController
{
}
For the moment, this is a class like any other. To have API endpoints, the TodosController class must inherit the ControllerBase class built into .NET.
And have the [ApiController] attribute and the controller route:
[ApiController]
[Route("api/")] // <-- controller route
public class TodosController : ControllerBase
{
}
Endpoints
The next step is to create API endpoints that will be called from the outside. An is nothing more than a class method with some additional attributes:
Regular method:
public int GetOne()
{
return 1;
}
Endpoint method:
[ApiController]
[Route("api/")]
public class TodosController : ControllerBase
{
[HttpGet("one")] // <-- route and action method
public int GetOne()
{
return 1;
}
}
The difference here is the [HttpGet("one")] attribute that indicates the action method ('GET') and the route name ('one').
After running the app, you can verify that the route is present in the Swagger and returns the expected result with the status code 200:

This also works in the browser:
https://localhost:44385/api/one
Some key points:
- A method can return any data type, string, int, object, etc.
- A method name can be anything.
- Likewise, a route name can also be anything you want.
- The route name shouldn't overlap with another route in the same controller. If there is an overlap, the method with the order of precedence will be invoked for both routes.
Using HTTP Status Codes
To use HTTP Status codes on the endpoint method, wrap the response in the ActionResult<T>.
[HttpGet("one")]
public ActionResult<int> GetOne()
{
return 1;
}
With this in place, you can return any status code you desire.
200
// response: 200
[HttpGet("one")]
public ActionResult<int> GetOne()
{
return Ok();
}
The method Ok() comes from the ControllerBase class you inherited earlier and produces the 200 status code response:
// Peek inside the ControllerBase class
public virtual OkObjectResult Ok([ActionResultObjectValue] object? value)
=> new OkObjectResult(value);
It can also accept any parameter (of primitive or complex type):
// response: 200
[HttpGet("one")]
public ActionResult<int> GetOne()
{
return Ok(new Todo { Title = "Title" });
}
The same applies to other status code methods.
400
// response: 400
[HttpGet("one")]
public ActionResult<int> GetOne()
{
return BadRequest();
}
401
// response: 401
[HttpGet("one")]
public ActionResult<int> GetOne()
{
return Unauthorized();
}
404
// response: 404
[HttpGet("one")]
public ActionResult<int> GetOne()
{
return NotFound();
}
Alternative approach
You can also return the status by using the StatusCode() method:
// response: 500
[HttpGet("one")]
public ActionResult<int> GetOne()
{
return StatusCode(500, "Optional Error Message");
}
ActionResult & IActionResult
You might be asking yourself how, before, when I didn't use status codes, a 200 status was in the response. That's because of two things:
- 200 status code is the default response code.
- The use of
ActionResult<T>response class
There is also the IActionResult response type that is commonly used. To demonstrate the difference, I'll create two endpoints with similar response types:
[HttpGet("true")]
public ActionResult<bool> GetTrue()
{
return true;
}
[HttpGet("false")]
public IActionResult GetFalse()
{
return Ok(false);
}
ActionResult<T>allows returning either a value of typeTor anyIActionResult. When returningTdirectly, ASP.NET Core automatically wraps it in a200 OKresponse unless otherwise specified.IActionResultrequires returning an object that implementsIActionResult. Primitive values cannot be returned directly, and the response must be wrapped (e.g.,Ok(value),JsonResult,ObjectResult).ActionResult<T>requires specifying the correct generic type so the framework can serialize the response and generate accurate OpenAPI metadata.IActionResultdoes not require specifying a response type in the method signature because it represents any possible HTTP response.
Regarding the API caller, there is no difference in the response when using IActionResult or ActionResult<T>.
Dynamic route parameters
The API endpoint can accept parameters in various forms:
As Route Parameters
The parameter in the route is specified in the GetUserByID() method using the [FromRoute] attribute.
[HttpGet("GetTodoByID/{todoID}")]
public async ActionResult<string> GetUserByID([FromRoute] int todoID)
{
return $"Todo ID is: {todoID}";
}
curl > http://localhost:44385/GetTodoByID/47
Todo ID is: 47
As Query Object
public class SearchQuery
{
public string? Name { get; set; }
public int? Age { get; set; }
}
When working with query objects, you only add [FromQuery] inside the Controller method, while the route stays intact.
[HttpGet("SearchUsers")]
public async ActionResult<string> GetUserByID([FromQuery] SearchQuery query)
{
if (query.Name is not null)
{
return users.FirstOrDefault(u => u.Name == query.Name);
}
if (query.Age is not null)
{
return users.FirstOrDefault(u => u.Age == query.Age);
}
return StatusCode(404, "User not found");
}
As Request Body
The [FromBody] attribute is used to accept the incoming request body. Fair warning, Request Body does not apply to GET requests.
[HttpPost("CreateUser")]
public async Task<ActionResult<UserDTO>> CreateUser([FromBody] UserDTO dto)
{
...
}
As Request Headers
The [FromHeader] attribute reads headers.
[HttpPost("SendHeaders")]
public async Task<ActionResult<UserDTO>> SendHeaders([FromHeader] RequestHeaders headers)
{
...
}
You can also use multiple attributes on an endpoint at once.
Asynchronous Operations
API controllers support asynchronous processing. This is done using the Task library that is built into .NET.
Wrap the response type with the Task class Task<ActionResult<User>> GetUserByID() and prefix it with async.
Then you can use await within your controller methods:
[HttpGet("GetUserByID/{userID}")]
public async Task<ActionResult<User>> GetUserByID([FromRoute] int userID)
{
// Simulate delay
await Task.Delay(3000);
// Simulate calling service that returns a Task<T>
var user = await GetUserByID(userID);
return user;
}
Documenting endpoints
It's always a good idea to document endpoints for other developers or Swagger users. One way to enrich endpoints is to use attributes:
[HttpGet("GetUserByID/{userID}")]
[ProducesResponseType(typeof(User), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(GenericErrorClass), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(GenericErrorClass), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(GenericErrorClass), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<User>> GetUserByID([FromRoute] int userID)
{
try
{
if (userID < 0) {
var errorResponse = new GenericErrorClass { Message = "Invalid User ID!" };
return StatusCode(400, errorResponse);
}
var user = await GetUserByID(userID);
if (user is null) {
var errorResponse = new GenericErrorClass { Message = "User not found!" };
return StatusCode(404, errorResponse);
}
return user;
}
catch (Exception ex)
{
var errorResponse = new GenericErrorClass { Message = "Something went wrong!" };
return StatusCode(500, errorResponse);
}
}
public class GenericErrorClass
{
public string Message { get; set; }
}
In addition, you can write summary comments that will be visible in Swagger using the Swashbuckle NuGet package.
/// <summary>
/// Retrieves User By ID
/// </summary>
/// <param name="userID">User ID from users table.</param>
/// <returns>User</returns>
[HttpGet("GetUserByID/{userID}")]
[ProducesResponseType(typeof(User), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(GenericErrorClass), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(GenericErrorClass), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(GenericErrorClass), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<User>> GetUserByID([FromRoute] int userID)
{
try
{
if (userID < 0) {
var errorResponse = new GenericErrorClass { Message = "Invalid User ID!" };
return StatusCode(400, errorResponse);
}
var user = await GetUserByID(userID);
if (user is null) {
var errorResponse = new GenericErrorClass { Message = "User not found!" };
return StatusCode(404, errorResponse);
}
return user;
}
catch (Exception ex)
{
var errorResponse = new GenericErrorClass { Message = "Something went wrong!" };
return StatusCode(500, errorResponse);
}
}
Local testing
You can test your endpoints by clicking the green play button at the top. This will start the project and display all your endpoints in Swagger:
Final Words
This is just a small slice of .NET Web APIs. In the following chapters, I'll dive deeper into Dependency Injection, CRUD Operations, configuration, and more.








Top comments (0)