Back to: ASP.NET Core Web API Tutorials
4XX HTTP Status Codes in ASP.NET Core Web API
In this article, I will explain 4XX HTTP Status Codes in ASP.NET Core Web API Application with examples. Please read our previous article discussing 3XX HTTP Status Codes in ASP.NET Core Web API Applications.
4XX HTTP Status Codes
The 4XX HTTP status codes or HTTP status codes that start with 4 are client error response codes. They indicate that the request made by the client contains bad syntax or cannot be fulfilled for some reason related to the client’s request. These codes are essential for informing users or applications about issues that need to be addressed on the client side such as invalid input, lack of proper authentication, insufficient permissions, an unavailable or non-existent resource, or an unsupported HTTP method, etc. The following are some common 4XX HTTP Status Codes:
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
400 Bad Request HTTP Status Code
A 400 Bad Request status code means that the server cannot process the request due to a client-side error (e.g., malformed request syntax, invalid request parameters, missing required fields, etc.). When a request fails validation or contains missing/incorrect data, the server should return a 400 Bad Request.
Real-time Use Cases & Scenarios of 400 Bad Requests:
- Missing or Invalid Input Data: When creating a new product, if required fields like Name or Price are absent or invalid in the request body, the server can respond with a 400. For example, a JSON request missing the Name property when creating a product.
- Malformed JSON or XML: If the client sends a malformed JSON/XML request body that cannot be parsed, the server returns 400. For example, a JSON body with missing brackets or quotes that breaks the parsing.
- Validation Errors: When applying server-side validation (e.g., checking maximum length, numeric range, or format constraints), any violation typically triggers a 400 response. For example, a “quantity” field that must be a positive integer is sent as a negative number or a non-integer string.
How to Return 400 HTTP Status Code in ASP.NET Core Web API
In ASP.NET Core Web API, we can return a 400 Bad Request status code using the BadRequest() method. Additionally, we can include error details to inform the client about the nature of the error. The syntax is given below:
return BadRequest(ModelState); // Returns 400 Bad Request with validation errors
401 Unauthorized HTTP Status Code
The 401 Unauthorized status code indicates that the request has not been processed because it lacks valid authentication credentials for the target resource. The client must authenticate itself to get the requested response. This error typically happens when the client does not provide authentication details, such as API keys, valid tokens, or login credentials.
Real-time Use Cases & Scenarios of 401 Unauthorized Status Code
- Missing Authentication Token: A client tries to access a protected API endpoint without providing an authentication token. For example, accessing user profile information without logging in or without including a JWT in the request header.
- Expired Token: A client’s authentication token has expired, and they attempt to make an authenticated request. For example, using an expired JWT to fetch user-specific data.
- Invalid Credentials: A client provides incorrect authentication credentials. For example, supplying an invalid API key or incorrect username/password combination.
How to Return 401 HTTP Status Code in ASP.NET Core Web API
In ASP.NET Core, returning a 401 Unauthorized status can be handled automatically using authentication middleware. However, you can also manually return it using the Unauthorized() method. The following is the syntax:
return Unauthorized(“You must be logged in to access this resource.”); // Returns 401 Unauthorized
403 Forbidden HTTP Status Code
A 403 Forbidden status code indicates that the client’s credentials are understood but do not grant permission to access the requested resource. Unlike 401, it means the client is authenticated (or the server recognizes who they are) but still cannot access the resource due to insufficient privileges or permissions. That means the client does not have permission to access the requested resource.
Real-time Use Cases & Scenarios of 403 Forbidden Status Code
- Role-Based Access Control: A user with the “user” role attempts to access an admin-only endpoint. The server recognizes the user is authenticated but denies access because of insufficient role permissions.
- IP Whitelisting: A client tries to access resources that are restricted based on IP addresses or other criteria. For example, accessing internal APIs from an external network.
How to Return 403 HTTP Status Code in ASP.NET Core Web API
You can return a 403 Forbidden status using the Forbid() method. Additionally, you can implement authorization policies to handle permission checks. The syntax is given below:
return Forbid(“You do not have permission to access this resource.”); // Returns 403 Forbidden
404 Not Found HTTP Status Code
A 404 Not Found status code indicates that the server cannot find the requested resource. The resource either does not exist or is hidden from the client. It is one of the most common and familiar HTTP error codes, typically triggered when a user navigates to a broken or missing link. This response is often used when the server does not wish to reveal exactly why the request has been refused or when no other response is applicable.
Real-time Use Cases & Scenarios of 404 Not Found Status Code
- Resource Does Not Exist: A client requests a resource using an ID that doesn’t exist in the database. For example, fetching a product with an ID that isn’t present in the inventory system.
- Invalid API Endpoint: A user or client application tries to request a URL endpoint that doesn’t exist on the server because of a spelling mistake. For example, typing /api/producs instead of /api/products.
- Deleted Resources: A resource that previously existed has been deleted. For example, attempting to access a user profile after the user has been removed.
How to Return 404 HTTP Status Code in ASP.NET Core Web API
In ASP.NET Core Web API, we can return a 404 Not Found status using the NotFound() method. The syntax is given below:
return NotFound($”Product with ID {id} not found.”); // Returns 404 Not Found
405 Method Not Allowed HTTP Status Code
The 405 Method Not Allowed status code indicates that the request method is known by the server but is not supported by the target resource. This often happens when a client tries to use a method (e.g., PUT, DELETE) on an endpoint that only supports other methods (like GET and POST). The server must generate an Allow header field containing a list of the allowed methods.
Real-time Use Cases & Scenarios of 405 Method Not Allowed Status Code
- Unsupported HTTP Methods: A client sends a POST request to an endpoint that only supports GET. For example, trying to POST data to /api/products/{id} where only GET, and PUT are allowed.
- Read-only Endpoint: An API that only allows GET requests for a particular resource. If a client attempts a POST, PUT, or DELETE, the server should respond with 405.
- Misconfigured Endpoints: An API endpoint is configured to accept only specific HTTP methods. For example, a /api/login endpoint that only accepts POST requests but receives a GET request.
- API Versioning Issues: A specific API version does not support certain methods. For example, version 1 of an API does not support DELETE, but the client attempts to use it.
How to Return 405 HTTP Status Code in ASP.NET Core Web API
ASP.NET Core automatically handles unsupported HTTP methods by returning a 405 Method Not Allowed status if the route matches but the HTTP method is not handled by any action. However, you can also manually handle it using the StatusCode method and specifying the allowed methods in the Allow header. The syntax is given below:
// Returns 405 Method Not Allowed with Allow header Response.Headers.Add("Allow", "GET"); return StatusCode(405, "POST method is not allowed on this endpoint.");
Example to Understand 4XX HTTP Status Codes in ASP.NET Core Web API
Let’s create an ASP.NET Core Web API application that demonstrates the usage of the 4XX HTTP status codes: 400, 401, 403, 404, and 405. So, first, create an ASP.NET Core Web API Project and give the name as HTTPStatusCodeDemo. Once you create the Project, please install the following packages using Package Manager Console in Visual.
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.Tools
- Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
Note: This Microsoft.AspNetCore.Authentication.JwtBearer Package is required for JWT-Based Token Authentication. In this demo, we will be using JWT-Based Token Authentication, and in our coming sessions, we will discuss JWT in detail.
Define the User Model
First, create a folder named Models at the project root directory, and inside the Modes folder, create a class file named User.cs and then copy and paste the following code. This defines the User entity, which represents individual users of the application. It includes properties like ID, Username, Password, and Role. The Username property is enforced to be unique via the [Index] attribute.
using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; namespace HTTPStatusCodeDemo.Models { [Index(nameof(Username), Name = "IX_Username_Unique", IsUnique =true)] public class User { [Key] public int Id { get; set; } [Required] public string Username { get; set; } [Required] public string Password { get; set; } // In production, passwords should be hashed [Required] public string Role { get; set; } // e.g., "User", "Admin" } }
It serves as the basis for authentication and authorization, providing a clear structure for storing user credentials and roles. This model will be used by the ApplicationDbContext to interact with the database and by controllers to verify log in credentials and assign permissions.
Define the Product Model
Create a class file named Product.cs within the Models folder and then copy and paste the following code. This defines the Product entity, representing items that can be sold or managed within the application. It includes fields such as ID, Name, Price, Quantity, and Description.
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace HTTPStatusCodeDemo.Models { public class Product { [Key] public int Id { get; set; } [Required] public string Name { get; set; } [Required] [Column(TypeName ="decimal(18,2)")] public decimal Price { get; set; } [Required] public int Quantity { get; set; } public string? Description { get; set; } public DateTime LastModified { get; set; } public List<ProductReview> ProductReviews { get; set; } = new List<ProductReview>(); } }
It serves as the main data model for product-related operations. The controller will use this class to create, update, delete, and retrieve product information, while the database context maps this entity to the Products table.
Define the Product Review Model
Create a class file named ProductReview.cs within the Models folder and then copy and paste the following code. This model represents customer reviews for a particular product, including properties like ID, ProductId (foreign key), Reviewer, Comment, and Rating.
using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; namespace HTTPStatusCodeDemo.Models { public class ProductReview { [Key] public int Id { get; set; } [Required] public int ProductId { get; set; } // Foreign Key public Product Product { get; set; } // Reference Navigation Property [Required] public string Reviewer { get; set; } [Required] public string Comment { get; set; } [Range(1, 5)] [Required] public int Rating { get; set; } } }
This model is used to store and retrieve feedback provided by customers. This class helps maintain a relational structure where reviews are tied to specific products, enabling the application to present product feedback and ratings to users.
Defining the ErrorResponseDTO Class
A well-structured error response should provide clear information about the error, including the HTTP status code, a user-friendly message, and optional detailed error information for debugging purposes.
First, create a folder named DTOs, and then inside the DTOs folder, create a class file named ErrorResponseDTO.cs and copy and paste the following code. This Data Transfer Object (DTO) is designed to standardize error responses. It includes StatusCode, Message, and optional Details properties.
namespace HTTPStatusCodeDemo.DTOs { public class ErrorResponseDTO { // The HTTP status code. public int StatusCode { get; set; } // A brief message describing the error. public string Message { get; set; } // Optional detailed information about the error (e.g., validation errors). public object? Details { get; set; } public ErrorResponseDTO(int statusCode, string message, object? details = null) { StatusCode = statusCode; Message = message; Details = details; } } }
This DTO is used to provide a uniform structure for returning error information to clients, making it easier for both developers and users to understand what went wrong. This improves maintainability and clarity when handling exceptions and returning meaningful error responses.
Product Create DTO
Create a class file named ProductCreateDTO.cs within the DTOs folder and then copy and paste the following code. This DTO defines the data structure required to create a new product. It contains properties like Name, Price, Quantity, and an optional Description.
using System.ComponentModel.DataAnnotations; namespace HTTPStatusCodeDemo.DTOs { public class ProductCreateDTO { [Required] public string Name { get; set; } [Required] public decimal Price { get; set; } [Required] public int Quantity { get; set; } public string? Description { get; set; } } }
Separating this DTO from the main Product entity ensures that only the necessary fields are provided and helps maintain the integrity of incoming data.
Product Update DTO
Create a class file named ProductUpdateDTO.cs within the DTOs folder and then copy and paste the following code. This DTO specifies the data structure required to update an existing product. It includes fields like ID, Name, Price, Quantity, and an optional Description.
using System.ComponentModel.DataAnnotations; namespace HTTPStatusCodeDemo.DTOs { public class ProductUpdateDTO { [Required] public int Id { get; set; } [Required] public string Name { get; set; } [Required] public decimal Price { get; set; } [Required] public int Quantity { get; set; } public string? Description { get; set; } } }
This DTO is used to validate and structure the incoming data for product updates. It ensures that the ID is always provided and that other fields are properly formatted, reducing the likelihood of errors during update operations.
Product Review Create DTO
Create a class file named ProductReviewCreateDTO.cs within the DTOs folder and then copy and paste the following code. This DTO defines the structure for creating a new product review, including properties like ProductId, Reviewer, Comment, and Rating.
using System.ComponentModel.DataAnnotations; namespace HTTPStatusCodeDemo.DTOs { public class ProductReviewCreateDTO { [Required] public int ProductId { get; set; } [Required] public string Reviewer { get; set; } [Required] public string Comment { get; set; } [Range(1, 5)] [Required] public int Rating { get; set; } } }
This DTO is used to validate and format review data before it’s added to the database. Using a separate DTO ensures that only the relevant fields are processed and simplifies input validation for review creation.
Product Review DTO
Create a class file named ProductReviewDTO.cs within the DTOs folder and then copy and paste the following code. It is a read-only DTO for returning review data to clients. It includes properties like Id, ProductId, Reviewer, Comment, and Rating.
namespace HTTPStatusCodeDemo.DTOs { public class ProductReviewDTO { public int Id { get; set; } public int ProductId { get; set; } public string Reviewer { get; set; } public string Comment { get; set; } public int Rating { get; set; } } }
This DTO is used to structure review data for client consumption. It is used by the controller to present reviews in a consistent and user-friendly format without exposing unnecessary internal details.
Product DTO
Create a class file named ProductDTO.cs within the DTOs folder and then copy and paste the following code. A DTO for returning product data to clients, including ID, Name, Price, Quantity, Description, LastModified, and a list of ProductReviews.
namespace HTTPStatusCodeDemo.DTOs { public class ProductDTO { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public int Quantity { get; set; } public string? Description { get; set; } public DateTime LastModified { get; set; } public List<ProductReviewDTO> ProductReviews { get; set; } = new List<ProductReviewDTO>(); } }
This DTO serves as the standardized response model for product-related queries. It provides all relevant product details, including associated reviews, in a clear and consistent format, making it easier for clients to display or process the information.
Login DTO
Create a class file named LoginDTO.cs within the DTOs folder and then copy and paste the following code. This DTO defines the structure for user login requests, with fields like Username and Password. This DTO ensures that only the necessary fields are provided during authentication, making it easier to validate user credentials and generate tokens.
using System.ComponentModel.DataAnnotations; namespace HTTPStatusCodeDemo.DTOs { public class LoginDTO { [Required] public string Username { get; set; } [Required] public string Password { get; set; } } }
Configure the Application DbContext
First, create a folder named Data at the project root directory and then inside the Data folder, create a class file named ApplicationDbContext.cs and then copy and paste the following code. This class acts as the bridge between the application and the database, providing DbSet properties for Users, Products, and ProductReviews. It also contains logic for seeding initial data.
using HTTPStatusCodeDemo.Models; using Microsoft.EntityFrameworkCore; namespace HTTPStatusCodeDemo.Data { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } // Seed initial data protected override void OnModelCreating(ModelBuilder modelBuilder) { // Seed Users modelBuilder.Entity<User>().HasData( new User { Id = 1, Username = "Pranaya@Example.com", Password = "AdminPass123", Role = "Admin" }, new User { Id = 2, Username = "Rout@Example.com", Password = "UserPass123", Role = "User" } ); // Seed Products modelBuilder.Entity<Product>().HasData( new Product { Id = 1, Name = "Laptop", Price = 20000, Quantity = 40, Description = "High-performance laptop", LastModified = new DateTime(2025, 1, 18, 0, 0, 0, DateTimeKind.Utc) }, new Product { Id = 2, Name = "Smartphone", Price = 10000, Quantity = 50, Description = "Latest model smartphone", LastModified = new DateTime(2025, 1, 18, 0, 0, 0, DateTimeKind.Utc) } ); // Seed ProductReviews modelBuilder.Entity<ProductReview>().HasData( // Reviews for Product ID 1 (Laptop) new ProductReview { Id = 1, ProductId = 1, Reviewer = "John Doe", Comment = "Excellent laptop, very fast and reliable.", Rating = 5 }, new ProductReview { Id = 2, ProductId = 1, Reviewer = "Jane Smith", Comment = "Good performance but the battery life could be better.", Rating = 4 }, // Reviews for Product ID 2 (Smartphone) new ProductReview { Id = 3, ProductId = 2, Reviewer = "Alice Johnson", Comment = "Loving the new smartphone! Great camera quality.", Rating = 5 }, new ProductReview { Id = 4, ProductId = 2, Reviewer = "Bob Brown", Comment = "Great features but a bit overpriced.", Rating = 3 } ); } public DbSet<User> Users { get; set; } public DbSet<Product> Products { get; set; } public DbSet<ProductReview> ProductReviews { get; set; } } }
To enable Entity Framework Core to handle database operations. It provides an abstraction layer over the database schema, enabling the application to interact with data as objects and handle migrations, queries, and data changes efficiently.
Modify appsettings.json
Make sure to add a connection string to your appsettings.json file. So, please modify the appsettings.json file as follows. Here, also we are adding a key for JWT.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "EFCoreDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ECommerceDB;Trusted_Connection=True;TrustServerCertificate=True;" }, "Jwt": { "Key": "KHPK6Ucf/zjvU4qW8/vkuuGLHeIo0l9ACJiTaAPLKbk=" //Secret Key } }
Configure Authentication and Authorization in Program Class:
The entry point for the application, responsible for configuring services, middleware, and request-handling pipelines. Here, it sets up essential services (e.g., authentication, database context), defines policies, and maps controllers. It also configures Swagger for API documentation and ensures that the application is ready to handle requests. So, please modify the Program class as follows:
using HTTPStatusCodeDemo.Data; using HTTPStatusCodeDemo.DTOs; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using System.Net; using System.Text; using System.Text.Json; namespace HTTPStatusCodeDemo { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the dependency injection container. // Add controller services and configure JSON options builder.Services.AddControllers() .AddJsonOptions(options => { // Disable the default camelCase naming policy to preserve property names as defined options.JsonSerializerOptions.PropertyNamingPolicy = null; }); // Add services for generating Swagger/OpenAPI documentation // Swagger is useful for API documentation and testing builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Configure the application's database context to use SQL Server builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( // Retrieve the connection string named "EFCoreDBConnection" from the configuration builder.Configuration.GetConnectionString("EFCoreDBConnection") ) ); // Configure Authentication using JWT (JSON Web Tokens) // Retrieve the JWT secret key from configuration settings (e.g., appsettings.json) // If not found, use a default hard-coded key (Note: Hard-coding secrets is not recommended for production) var jwtSecret = builder.Configuration["Jwt:Key"] ?? "KHPK6Ucf/zjvU4qW8/vkuuGLHeIo0l9ACJiTaAPLKbk="; // Should be stored securely // Convert the JWT secret key into a byte array, which is required for token signing var key = Encoding.ASCII.GetBytes(jwtSecret); // Add authentication services and configure JWT Bearer options builder.Services.AddAuthentication(options => { // Set the default authentication scheme to JWT Bearer options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; //options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { // Configure JWT Bearer options // Disable HTTPS metadata requirement (Set to true in production to ensure tokens are transmitted securely) options.RequireHttpsMetadata = false; // Set to true in production // Save the token in the authentication properties after a successful authorization // options.SaveToken = true; // Define the parameters for validating incoming JWT tokens options.TokenValidationParameters = new TokenValidationParameters { // Enable validation of the token's signing key ValidateIssuerSigningKey = true, // Specify the signing key to validate against (must match the key used to generate the token) IssuerSigningKey = new SymmetricSecurityKey(key), // Disable issuer validation (Set to true and specify valid issuers in production) ValidateIssuer = false, // Disable audience validation (Set to true and specify valid audiences in production) ValidateAudience = false }; // Customize the responses for authentication events such as challenges and forbidden access options.Events = new JwtBearerEvents { // Event triggered when an authentication challenge occurs (e.g., invalid or missing token) OnChallenge = context => { // Prevent the default behavior (which may include redirecting to a login page) context.HandleResponse(); // Check if the response has already started if (!context.Response.HasStarted) { // Set the response status code to 401 Unauthorized context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; // Set the response content type to JSON context.Response.ContentType = "application/json"; // Create an error response DTO with status code 401 and an error message var errorResponse = new ErrorResponseDTO( statusCode: 401, message: "Authentication failed. Please provide valid credentials." ); // Serialize the error response DTO to JSON var json = JsonSerializer.Serialize(errorResponse); // Write the JSON error response to the HTTP response body return context.Response.WriteAsync(json); } // If the response has already started, do nothing return Task.CompletedTask; }, // Event triggered when access is forbidden (e.g., user lacks necessary permissions) OnForbidden = context => { // Check if the response has already started if (!context.Response.HasStarted) { // Set the response status code to 403 Forbidden context.Response.StatusCode = (int)HttpStatusCode.Forbidden; // Set the response content type to JSON context.Response.ContentType = "application/json"; // Create an error response DTO with status code 403 and an error message var errorResponse = new ErrorResponseDTO( statusCode: 403, message: "You do not have permission to access this resource." ); // Serialize the error response DTO to JSON var json = JsonSerializer.Serialize(errorResponse); // Write the JSON error response to the HTTP response body return context.Response.WriteAsync(json); } // If the response has already started, do nothing return Task.CompletedTask; } }; }); // Configure Authorization Policies builder.Services.AddAuthorization(options => { // Define a policy named "AdminOnly" that requires the user to have the "Admin" role options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin")); }); // Build the WebApplication instance var app = builder.Build(); // Configure the HTTP request pipeline. // If the application is running in the Development environment, enable Swagger for API documentation if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } // Enforce the use of HTTPS by redirecting HTTP requests to HTTPS app.UseHttpsRedirection(); // Enable Authentication middleware to validate and set the user principal for requests app.UseAuthentication(); // Must come before UseAuthorization // Enable Authorization middleware to enforce access policies app.UseAuthorization(); // Map controller endpoints (attribute routing) app.MapControllers(); app.Run(); } } }
Implement Authentication Controller
Create an API Empty Controller named AuthController within the Controllers folder and then copy and paste the following code. This class manages user authentication. It includes an endpoint for logging in, validating credentials, and generating JWT tokens. It is used to handle user login and provide clients with tokens for subsequent authenticated requests. It ensures that only valid users receive a token, enhancing security and simplifying the user authentication process.
using HTTPStatusCodeDemo.Data; using HTTPStatusCodeDemo.DTOs; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace HTTPStatusCodeDemo.Controllers { [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { // Private field to hold the application's database context private readonly ApplicationDbContext _context; // Private field to store the JWT secret key private readonly string _jwtSecret; // Constructor that injects the database context and configuration settings public AuthController(ApplicationDbContext context, IConfiguration configuration) { _context = context; // Assign the injected database context to the private field // Retrieve the JWT secret key from configuration; if not set, use a default value _jwtSecret = configuration["Jwt:Key"] ?? "KHPK6Ucf/zjvU4qW8/vkuuGLHeIo0l9ACJiTaAPLKbk="; } // HTTP POST endpoint for user login at "api/auth/login" [HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginDTO login) { // Check if the incoming model state is valid based on data annotations if (!ModelState.IsValid) { // Create an error response DTO with status code 400 and an error message var errorResponse = new ErrorResponseDTO( statusCode: 400, message: $"Invalid login request." ); // Return a 400 Bad Request response with the error details return BadRequest(errorResponse); } // Attempt to find a user in the database matching the provided username and password var user = await _context.Users .FirstOrDefaultAsync(u => u.Username == login.Username && u.Password == login.Password); // If no matching user is found, return an unauthorized response if (user == null) { // Create an error response DTO with status code 401 and an error message var errorResponse = new ErrorResponseDTO( statusCode: 401, message: $"Invalid username or password." ); // Return a 401 Unauthorized response with the error details return Unauthorized(errorResponse); } // Initialize a JWT token handler to create and write tokens var tokenHandler = new JwtSecurityTokenHandler(); // Convert the JWT secret key into a byte array var key = Encoding.ASCII.GetBytes(_jwtSecret); // Define the token's descriptor, including claims, expiration, and signing credentials var tokenDescriptor = new SecurityTokenDescriptor { // Define the claims for the token, such as the user's name and role Subject = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, user.Username), // Claim for the user's name new Claim(ClaimTypes.Role, user.Role) // Claim for the user's role }), // Set the token's expiration time to 1 hour from now Expires = DateTime.UtcNow.AddHours(1), // Define the signing credentials using the secret key and HMAC SHA256 algorithm SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(key), // Uses the encoded secret key for signing SecurityAlgorithms.HmacSha256Signature // Specifies the signing algorithm (HMAC-SHA256) ) }; // Create the security token based on the descriptor var token = tokenHandler.CreateToken(tokenDescriptor); // Serialize the token to a JWT string var jwtToken = tokenHandler.WriteToken(token); // Return a 200 OK response with the generated JWT token return Ok(new { Token = jwtToken }); } } }
Implement Products Controller with 4XX Status Codes
Create an API Empty Controller named ProductsController within the Controllers folder and then copy and paste the following code. This Controller provides CRUD operations for products and manages associated reviews. It also demonstrates how to handle various HTTP status codes (400, 401, 403, 404, 405). It handles product-related API requests, including listing all products, retrieving a specific product, creating new products, updating existing ones, deleting products, and managing reviews. It serves as the main interface for product management in the application.
using HTTPStatusCodeDemo.Data; using HTTPStatusCodeDemo.DTOs; using HTTPStatusCodeDemo.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace HTTPStatusCodeDemo.Controllers { [ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly ApplicationDbContext _context; public ProductsController(ApplicationDbContext context) { _context = context; } // Retrieves all products along with their reviews. // Returns a List of products with 200 OK. [HttpGet("GetProductsAsync")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<ProductDTO>))] [ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(ErrorResponseDTO))] public async Task<ActionResult<List<ProductDTO>>> GetProductsAsync() { try { var productDTOs = await _context.Products .AsNoTracking() .Select(p => new ProductDTO { Id = p.Id, Name = p.Name, Description = p.Description, Price = p.Price, Quantity = p.Quantity, LastModified = p.LastModified, ProductReviews = p.ProductReviews.Select(r => new ProductReviewDTO { Id = r.Id, ProductId = r.ProductId, Reviewer = r.Reviewer, Comment = r.Comment, Rating = r.Rating }).ToList() }) .ToListAsync(); return Ok(productDTOs); // 200 OK } catch (Exception ex) { // Log the exception var errorResponse = new ErrorResponseDTO( statusCode: 500, message: "An unexpected error occurred.", details: ex.Message // In production, avoid exposing exception details ); return StatusCode(500, errorResponse); } } // Retrieves a specific product by ID along with its reviews. // Returns Product details with 200 OK or 404 Not Found. [HttpGet("GetProductByIdAsync/{id}")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ProductDTO))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponseDTO))] [ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(ErrorResponseDTO))] public async Task<ActionResult<ProductDTO>> GetProductByIdAsync(int id) { try { // Project directly into ProductDTO var productDto = await _context.Products .AsNoTracking() // Use AsNoTracking to avoid tracking entities .Where(p => p.Id == id) .Select(p => new ProductDTO { Id = p.Id, Name = p.Name, Price = p.Price, Quantity = p.Quantity, Description = p.Description, LastModified = p.LastModified, ProductReviews = p.ProductReviews.Select(r => new ProductReviewDTO { Id = r.Id, ProductId = r.ProductId, Reviewer = r.Reviewer, Comment = r.Comment, Rating = r.Rating }).ToList() }) .FirstOrDefaultAsync(); if (productDto == null) { var errorResponse = new ErrorResponseDTO( statusCode: 404, message: $"Product with ID {id} not found." ); return NotFound(errorResponse); // 404 Not Found } return Ok(productDto); // 200 OK } catch (Exception ex) { var errorResponse = new ErrorResponseDTO( statusCode: 500, message: "An unexpected error occurred.", details: ex.Message ); return StatusCode(500, errorResponse); // 500 Internal Server Error } } // Creates a new product. // Returns 201 Created with product or 400 Bad Request. [HttpPost("CreateProductAsync")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ProductDTO))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponseDTO))] [ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(ErrorResponseDTO))] public async Task<ActionResult<ProductDTO>> CreateProductAsync([FromBody] ProductCreateDTO productCreateDTO) { try { if (!ModelState.IsValid) { var errorResponse = new ErrorResponseDTO( statusCode: 400, message: "Invalid product data.", details: ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage) ); return BadRequest(errorResponse); } var newProduct = new Product { Name = productCreateDTO.Name, Price = productCreateDTO.Price, Quantity = productCreateDTO.Quantity, Description = productCreateDTO.Description, LastModified = DateTime.UtcNow }; _context.Products.Add(newProduct); await _context.SaveChangesAsync(); var productDTO = new ProductDTO { Id = newProduct.Id, Name = newProduct.Name, Price = newProduct.Price, Quantity = newProduct.Quantity, Description = newProduct.Description, LastModified = newProduct.LastModified }; return Ok(productDTO); // 200 Ok } catch (Exception ex) { var errorResponse = new ErrorResponseDTO( statusCode: 500, message: "An unexpected error occurred.", details: ex.Message ); return StatusCode(500, errorResponse); } } // Updates an existing product. // Returns 204 No Content, 400 Bad Request, or 404 Not Found. [HttpPut("UpdateProductAsync/{id}")] [Authorize(Roles = "Admin")] // Requires Admin role [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponseDTO))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponseDTO))] [ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(ErrorResponseDTO))] public async Task<IActionResult> UpdateProductAsync(int id, [FromBody] ProductUpdateDTO productUpdateDTO) { try { if (id != productUpdateDTO.Id) { var errorResponse = new ErrorResponseDTO( statusCode: 400, message: "Product ID mismatch." ); return BadRequest(errorResponse); // 400 Bad Request } if (!ModelState.IsValid) { var errorResponse = new ErrorResponseDTO( statusCode: 400, message: "Invalid product data.", details: ModelState.Values.SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) ); return BadRequest(errorResponse); // 400 Bad Request } var product = await _context.Products.FindAsync(id); if (product == null) { var errorResponse = new ErrorResponseDTO( statusCode: 404, message: $"Product with ID {id} not found." ); return NotFound(errorResponse); // 404 Not Found } // Update product details product.Name = productUpdateDTO.Name; product.Description = productUpdateDTO.Description; product.Price = productUpdateDTO.Price; product.Quantity = productUpdateDTO.Quantity; product.LastModified = DateTime.UtcNow; _context.Products.Update(product); await _context.SaveChangesAsync(); return NoContent(); // 204 No Content } catch (Exception ex) { var errorResponse = new ErrorResponseDTO( statusCode: 500, message: "An unexpected error occurred.", details: ex.Message ); return StatusCode(500, errorResponse); } } // Deletes a product by ID. // Returns 204 No Content or 404 Not Found. [HttpDelete("DeleteProductAsync/{id}")] [Authorize(Roles = "Admin")] // Requires Admin role [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponseDTO))] [ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(ErrorResponseDTO))] public async Task<IActionResult> DeleteProductAsync(int id) { try { var product = await _context.Products .Include(p => p.ProductReviews) .FirstOrDefaultAsync(p => p.Id == id); if (product == null) { var errorResponse = new ErrorResponseDTO( statusCode: 404, message: $"Product with ID {id} not found." ); return NotFound(errorResponse); // 404 Not Found } // Optionally, delete related reviews if (product.ProductReviews != null && product.ProductReviews.Any()) { _context.ProductReviews.RemoveRange(product.ProductReviews); } _context.Products.Remove(product); await _context.SaveChangesAsync(); return NoContent(); // 204 No Content } catch (Exception ex) { var errorResponse = new ErrorResponseDTO( statusCode: 500, message: "An unexpected error occurred.", details: ex.Message ); return StatusCode(500, errorResponse); // 500 Internal Server Error } } // Adds a review to a specific product. // Returns 200 OK with review, 400 Bad Request, or 404 Not Found. [HttpPost("AddReviewToProductAsync")] [Authorize] // Requires authentication (optional, based on requirements) [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ProductReviewDTO))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponseDTO))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponseDTO))] [ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(ErrorResponseDTO))] public async Task<ActionResult<ProductReviewDTO>> AddReviewToProductAsync([FromBody] ProductReviewCreateDTO productReviewDTO) { try { if (!ModelState.IsValid) { var errorResponse = new ErrorResponseDTO( statusCode: 400, message: "Invalid review data.", details: ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage) ); return BadRequest(errorResponse); // 400 Bad Request } var product = await _context.Products.FindAsync(productReviewDTO.ProductId); if (product == null) { var errorResponse = new ErrorResponseDTO( statusCode: 404, message: $"Product with ID {productReviewDTO.ProductId} not found." ); return NotFound(errorResponse); // 404 Not Found } ProductReview productReview = new ProductReview() { // Associate the review with the product ProductId = productReviewDTO.ProductId, Comment = productReviewDTO.Comment, Reviewer = productReviewDTO.Reviewer, Rating = productReviewDTO.Rating }; _context.ProductReviews.Add(productReview); await _context.SaveChangesAsync(); // Manually map the created ProductReview to a ProductReviewDTO var productReviewDTOResponse = new ProductReviewDTO { Id = productReview.Id, ProductId = productReview.ProductId, Reviewer = productReview.Reviewer, Comment = productReview.Comment, Rating = productReview.Rating }; return Ok(productReviewDTOResponse); // 200 OK } catch (Exception ex) { var errorResponse = new ErrorResponseDTO( statusCode: 500, message: "An unexpected error occurred.", details: ex.Message ); return StatusCode(500, errorResponse); // 500 Internal Server Error } } // Retrieves all reviews for a specific product // Returns List of reviews with 200 OK or 404 Not Found [HttpGet("GetReviewsByProductIdAsync/{productId}")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<ProductReviewDTO>))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponseDTO))] [ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(ErrorResponseDTO))] public async Task<ActionResult<List<ProductReviewDTO>>> GetReviewsByProductIdAsync(int productId) { try { // Directly project into ProductReviewDTO and use AsNoTracking for better performance var reviewDTOs = await _context.ProductReviews .AsNoTracking() // No tracking for better performance .Where(rev => rev.ProductId == productId) .Select(rev => new ProductReviewDTO { Id = rev.Id, ProductId = rev.ProductId, Reviewer = rev.Reviewer, Comment = rev.Comment, Rating = rev.Rating }) .ToListAsync(); // Check if no reviews were found if (!reviewDTOs.Any()) { var errorResponse = new ErrorResponseDTO( statusCode: 404, message: $"Reviews for Product ID {productId} not found." ); return NotFound(errorResponse); // 404 Not Found } return Ok(reviewDTOs); // 200 OK } catch (Exception ex) { var errorResponse = new ErrorResponseDTO( statusCode: 500, message: "An unexpected error occurred.", details: ex.Message ); return StatusCode(500, errorResponse); // 500 Internal Server Error } } // Handles unsupported HTTP methods for reviews. [HttpPut("reviews/{reviewId}")] [HttpDelete("reviews/{reviewId}")] [ProducesResponseType(StatusCodes.Status405MethodNotAllowed, Type = typeof(ErrorResponseDTO))] public IActionResult UnsupportedReviewMethod() { var allowedMethods = "GET, POST"; Response.Headers.Append("Allow", allowedMethods); var errorResponse = new ErrorResponseDTO( statusCode: 405, message: "This HTTP method is not allowed for the requested resource." ); return StatusCode(405, errorResponse); // 405 Method Not Allowed } } }
Database Migration
Next, we need to generate the Migration and update the database schema. So, open the Package Manager Console and Execute the Add-Migration and Update-Database commands as follows.
With this. our Database with Users, Products, and ProductReviews tables should have been created as shown in the below image:
Testing the Endpoints.
Now, run the application and test each endpoint and it should work as expected.
4XX HTTP Status Codes in ASP.NET Core Web API:
4XX HTTP status codes inform clients about client-side errors related to authentication, authorization, data validation, or usage of unsupported methods. Proper use of these status codes can greatly enhance API clarity by clearly communicating the reason a request fails. Understanding these status codes is critical for both developers and clients to debug and resolve issues effectively. By correctly handling these errors, API developers can guide clients toward correcting their requests, improving the overall user experience.
- 400 Bad Request: The client sent an invalid request (malformed syntax, validation errors, etc.).
- 401 Unauthorized: The request requires valid authentication credentials, or the provided credentials are invalid/expired.
- 403 Forbidden: The client is authenticated but does not have permission to access the resource.
- 404 Not Found: The requested resource cannot be found on the server.
- 405 Method Not Allowed: The resource endpoint does not support the HTTP method used.
In the next article, I will discuss 5XX HTTP Status Codes in ASP.NET Core Web API applications with Examples. In this article, I explain 4XX HTTP Status Codes in ASP.NET Core Web API application with multiple Examples. I hope you enjoy this article on “3XX HTTP Status Codes in ASP.NET Core Web API”.