Practical Training for Building Real AI Apps with MongoDB (Sponsored)
When you're trying to build anything involving RAG, vector search, or agent-style workflows, the biggest challenge is understanding how all the pieces fit together with your data. What stood out to me while going through the MongoDB AI Learning Hub is that it focuses less on marketing and more on giving developers a structured path to actually build things.
Here is what you can learn in the MongoDB Free Training:
Atlas Vector Search shows how to work with embeddings in a way that feels familiar if you already use MongoDB.
The guides break down what an AI stack looks like and how vector databases behave, which helps connect the concepts to real schema design.
The Skill Badges and walkthroughs focus on storing, indexing, and retrieving live data β the things that matter when you're aiming for production
What makes RAG reliable.
Agent-style patterns, which is helpful if you're experimenting with multi-step workflows or reasoning loops.
If you're already using MongoDB, the value is in seeing how these AI concepts map onto the database you have.
Your LLMs Are Getting Smarter β But Their Code Is Getting Harder to Trust (Sponsored)
AI models like GPT-5 and Claude Sonnet 4 are now achieving weighted pass rates of up to 80% on industry benchmarks, enabling teams to ship features significantly faster. Sonar's Coding Personalities of Leading LLMs report shows they also flood your repos with complex and verbose code, code smells, hidden bugs, and even BLOCKER-level security issues.
That means organizations should use these LLMs for speed, analysis, and complex problem-solving only when paired with strong automated verification, or the technical debt will overwhelm any productivity gains.
SonarQube closes this gap by adding a consistent quality and security layer around all AI and human-generated code, catching the hidden issues that slip past normal reviews, including concurrency bugs, vulnerabilities and security issues.
With real-time checks in the IDE, automated pull-request gates, and dashboards to track AI risk across teams, SonarQube lets you adopt the newest LLMs confidently while keeping your codebase safe, clean, and maintainable.
Make smarter AI adoption decisions with Sonar's latest report. Explore the habits, blind spots, and archetypes of the top six LLMs to uncover the critical risks each brings to your codebase.
Building REST APIs seems straightforward until you face real-world challenges.
Throughout my career, I have built over 100 APIs, and I have learned that following best practices from the start saves countless hours of refactoring later.
A well-designed REST API is predictable, maintainable, and easy to consume.
It follows consistent patterns that developers can understand without needing to read extensive documentation.
In this post, I will share the battle-tested practices I use when building REST APIs with ASP.NET Core.
These are not theoretical concepts but practical guidelines I apply in production systems handling millions of requests daily.
In this post, we will explore:
Understanding REST Maturity Levels
How to name resources correctly for clean API design
Choosing the correct HTTP methods
Choosing the correct status codes
Implementing API versioning strategies that work
Structuring requests and responses with proper standards
Leonard Richardson created a maturity model that breaks down REST adoption into four levels.
Level 0: The Swamp of POX (Plain Old XML)
At this level, you're using HTTP as a transport mechanism, nothing more. You have a single URI endpoint that typically uses only POST requests, and the request body contains all the information about what operation to perform.
This is essentially RPC (Remote Procedure Call) over HTTP. SOAP APIs often operate at this level.
Endpoint example: /api/service
Level 1: Resources
At this level, you start breaking down your single endpoint into multiple resource-based URIs.
Instead of having a single endpoint handle everything, you have different URIs for different resources.
Now you're using HTTP methods correctly.
GET for retrieving data, POST for creating, PUT for updating, and DELETE for removing.
You also use HTTP status codes meaningfully.
Endpoint examples:
GET /api/orders/123 - retrieve an order
POST /api/orders - create a new order
PUT /api/orders/123 - update an order
DELETE /api/orders/123 - delete an order
Proper status codes: 200 OK, 201 Created, 404 Not Found, etc.
Most APIs stop at Level 2, and it works.
Such APIs are predictable; they follow HTTP standards, and developers know how to build them.
Here is a typical API at Level 2, implemented with ASP.NET Core and Controllers:
csharp
1[ApiController]2[Route("api/orders")]3publicclassOrdersController:ControllerBase4{5privatereadonlyIOrderService _orderService;67publicOrdersController(IOrderService orderService)8{9 _orderService = orderService;10}1112[HttpGet("{id}")]13publicActionResult<Order>GetOrder(int id)14{15var order = _orderService.GetById(id);1617if(order isnull)18{19returnNotFound();20}2122returnOk(order);23}2425[HttpPost]26publicActionResult<Order>CreateOrder([FromBody]CreateOrderRequest request)27{28var order = _orderService.Create(request);2930returnCreatedAtAction(nameof(GetOrder),new{ id = order.Id }, order);31}3233[HttpPut("{id}")]34publicActionResult<Order>UpdateOrder(int id,[FromBody]UpdateOrderRequest request)35{36if(id != request.Id)37{38returnBadRequest("Id mismatch");39}4041var order = _orderService.Update(request);4243if(order isnull)44{45returnNotFound();46}4748returnOk(order);49}5051[HttpDelete("{id}")]52publicIActionResultDeleteOrder(int id)53{54var success = _orderService.Delete(id);5556if(!success)57{58returnNotFound();59}6061returnNoContent();62}63}
Level 3: Hypermedia Controls (HATEOAS)
This is true REST. Your API responses include links to related resources and available actions. The client doesn't need to know your URL structure upfront.
The server tells the client what it can do next.
You can read my article on HATEOAS to learn when you can benefit from HATEOAS and when it's an overkill.
How to Name Resources Correctly for Clean API Design
Resource naming is one of the most visible aspects of your API design.
Poor naming creates confusion and makes your API harder to use. Good naming makes your API intuitive and self-documenting.
Resources should represent things instead of actions.
The HTTP method (GET, POST, PUT, DELETE) indicates the action.
csharp
1// Bad - Using verbs in URLs2[HttpGet("api/getProducts")]3[HttpPost("api/createProduct")]4[HttpPut("api/updateProduct")]5[HttpDelete("api/deleteProduct")]67// Good - Using nouns with HTTP methods8[HttpGet("api/products")]9[HttpPost("api/products")]10[HttpPut("api/products/{id}")]11[HttpDelete("api/products/{id}")]
Collections should always use plural nouns for consistency:
csharp
1// Bad - Mixing singular and plural2[Route("api/product")]// Singular3[Route("api/orders")]// Plural45// Good - Always plural6[Route("api/products")]7[Route("api/orders")]8[Route("api/customers")]
This creates a predictable pattern. Even when getting a single item, you still use the plural form:
csharp
1[HttpGet("api/products/{id}")]// Get single product2[HttpGet("api/products")]// Get all products
The only exception is nouns that are always singular, such as software and localization.
Choose one naming convention and stick to it consistently throughout your entire API.
Lower kebab-case is recommended for multi-word resources, it's URL-friendly, easy to read, and widely adopted:
csharp
1// Good - Kebab-case (lowercase with hyphens)2[Route("api/product-categories")]3[Route("api/shopping-carts")]4[Route("api/customer-addresses")]5[Route("api/order-items")]67[ApiController]8[Route("api/product-categories")]9publicclassProductCategoriesController:ControllerBase10{11[HttpGet]12publicActionResult<IEnumerable<ProductCategory>>GetAll()13{14var categories = _categoryService.GetAll();15returnOk(categories);16}17}
When resources have clear parent-child relationships, reflect that in the URL structure:
csharp
1[ApiController]2[Route("api/orders")]3publicclassOrdersController:ControllerBase4{5// Get all orders6[HttpGet]7publicActionResult<IEnumerable<Order>>GetOrders()8{9var orders = _orderService.GetAll();10returnOk(orders);11}1213// Get specific order14[HttpGet("{orderId}")]15publicActionResult<Order>GetOrder(int orderId)16{17var order = _orderService.GetById(orderId);1819if(order isnull)20returnNotFound();2122returnOk(order);23}2425// Get items for specific order26[HttpGet("{orderId}/items")]27publicActionResult<IEnumerable<OrderItem>>GetOrderItems(int orderId)28{29var order = _orderService.GetById(orderId);3031if(order isnull)32returnNotFound();3334var items = _orderService.GetItems(orderId);35returnOk(items);36}3738// Get specific item from specific order39[HttpGet("{orderId}/items/{itemId}")]40publicActionResult<OrderItem>GetOrderItem(int orderId,int itemId)41{42var item = _orderService.GetItem(orderId, itemId);4344if(item isnull)45returnNotFound();4647returnOk(item);48}4950// Add item to order51[HttpPost("{orderId}/items")]52publicActionResult<OrderItem>AddOrderItem(53int orderId,54[FromBody]CreateOrderItemRequest request)55{56var order = _orderService.GetById(orderId);5758if(order isnull)59returnNotFound();6061var item = _orderService.AddItem(orderId, request);6263returnCreatedAtAction(64nameof(GetOrderItem),65new{ orderId = orderId, itemId = item.Id },66 item);67}68}
Avoid nesting more than two levels deep. Deep nesting creates long, complex URLs:
csharp
1// Bad - Too much nesting2[HttpGet("api/customers/{customerId}/orders/{orderId}/items/{itemId}/reviews/{reviewId}")]34// Good - Flatten the hierarchy5[HttpGet("api/reviews/{reviewId}")]6[HttpGet("api/order-items/{itemId}/reviews")]
If a resource can exist independently, give it its own top-level endpoint.
Here is a complete example showing consistent resource naming:
csharp
1// Products and categories2[Route("api/products")]3[Route("api/product-categories")]45// Customer management6[Route("api/customers")]7[Route("api/customers/{customerId}/addresses")]8[Route("api/customers/{customerId}/payment-methods")]910// Orders and shopping11[Route("api/orders")]12[Route("api/orders/{orderId}/items")]13[Route("api/shopping-carts")]14[Route("api/shopping-carts/{cartId}/items")]1516// Inventory17[Route("api/warehouses")]18[Route("api/warehouses/{warehouseId}/inventory")]1920// Reviews21[Route("api/products/{productId}/reviews")]22[Route("api/reviews/{reviewId}")]// Direct access when you have the ID
1// Good - Query parameters for filtering2[HttpGet("api/products?category=electronics&inStock=true")]3[HttpGet("api/orders?status=pending&customerId=123")]4[HttpGet("api/products?search=laptop")]56publicclassProductsController:ControllerBase7{8[HttpGet]9publicActionResult<IEnumerable<Product>>GetProducts(10[FromQuery]string category =null,11[FromQuery]bool? inStock =null,12[FromQuery]string search =null)13{14var products = _productService.GetFiltered(category, inStock, search);15returnOk(products);16}17}1819// Bad - Query parameters for actions20[HttpGet("api/products?action=delete&id=123")]21[HttpPost("api/orders?operation=cancel")]
Don't use query parameters for actions.
Query parameters refine which resources you want. The HTTP method and path determine the action.
Good resource naming makes your API predictable and easy to understand. Stick to these patterns consistently across your entire API.
HTTP methods and status codes are the foundation of RESTful communication.
Using them correctly makes your API predictable and allows clients to handle responses appropriately.
Using them incorrectly creates confusion and bugs.
1[HttpPost]2publicActionResult<Order>CreateOrder([FromBody]CreateOrderRequest request)3{4var order = _orderService.Create(request);56// 201 Created with Location header7returnCreatedAtAction(nameof(GetOrder),new{ id = order.Id }, order);8}
202 Accepted - Asynchronous Processing
Use when the request is accepted, but processing will be completed asynchronously:
csharp
1[HttpPost("process-bulk-import")]2publicIActionResultProcessBulkImport([FromBody]BulkImportRequest request)3{4var jobId = _importService.QueueImport(request);56returnAcceptedAtAction(7nameof(GetImportStatus),8new{ jobId = jobId },9new{ jobId = jobId, status ="processing"});// 202 Accepted10}1112[HttpGet("import-status/{jobId}")]13publicActionResult<ImportStatus>GetImportStatus(string jobId)14{15var status = _importService.GetStatus(jobId);16returnOk(status);17}
204 No Content - Success With No Response Body
Use for successful DELETE operations or PUT, PATCH that do not return data:
csharp
1[HttpDelete("{id}")]2publicIActionResultDeleteProduct(int id)3{4var deleted = _productService.Delete(id);56if(!deleted)7{8returnNotFound();9}1011returnNoContent();// 204 No Content12}
400 Bad Request: use when the request is malformed or contains invalid data.
401 Unauthorized: use when authentication is missing or invalid
403 Forbidden: use when the user is authenticated but lacks permission
404 Not Found: use when the requested resource does not exist
409 Conflict: use when the request conflicts with the current state of the resource
422 Unprocessable Entity: use when the request is well-formed but fails business validation
429 Too Many Requests: use when the client is sending too many requests in a given amount of time (Rate Limiting)
Example when the user is authenticated but lacks permission (403 Forbidden):
csharp
1[HttpDelete("{id}")]2publicIActionResultDeleteOrder(int id)3{4var order = _orderService.GetById(id);56if(order isnull)7{8returnNotFound();9}1011var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;1213// User can only delete their own orders14if(order.CustomerId.ToString()!= userId)15{16returnForbid();// 403 Forbidden17}1819 _orderService.Delete(id);20returnNoContent();21}
Example when the request is well-formed but fails business validation (422 Unprocessable Entity):
csharp
1[HttpPost("checkout")]2publicActionResult<Order>Checkout([FromBody]CheckoutRequest request)3{4var cart = _cartService.GetById(request.CartId);56if(cart isnull)7{8returnNotFound();9}1011// Business validation12var validation = _orderService.ValidateCheckout(cart);1314if(!validation.IsValid)15{16returnUnprocessableEntity(validation.Errors);// 42217}1819var order = _orderService.CompleteCheckout(cart, request);20returnOk(order);21}
APIs evolve over time, and you need a way to introduce changes without breaking existing clients.
API versioning allows you to maintain backward compatibility while adding new features or fixing design issues.
If you can avoid versioning, do it:
Add non-required fields to existing resources
Add new endpoints
If you need to introduce breaking changes, use versioning.
Note: When introducing a new version, plan to deprecate the old one.
csharp
1[ApiController]2[Route("api/v{version:apiVersion}/products")]3[ApiVersion("1.0", Deprecated =true)]4publicclassProductsV1Controller:ControllerBase5{6[HttpGet]7publicActionResult<IEnumerable<ProductV1>>GetProducts()8{9// Add deprecation warning in response headers10 Response.Headers.Add("X-API-Deprecation-Warning",11"This API version is deprecated. Please migrate to v2 by 2025-12-31");1213var products = _productService.GetAll();14returnOk(products);15}16}
The API versioning package automatically adds deprecation information to the response headers when you mark a version as deprecated.
Structuring Requests and Responses With Proper Standards
Consistent request and response structures make your API predictable and easy to integrate.
Standard formats reduce confusion and allow clients to handle responses uniformly across all endpoints.
1. Prefer Using JSON:
JSON is the standard format for REST APIs. It is readable, well-supported, and language-agnostic.
2. Use Consistent Property Naming:
Use camelCase for JSON properties and stick to it consistently throughout your entire API.
3. Standardize Error Responses With RFC 9457 Problem Details:
RFC 9457 (previously RFC 7807) defines a standard format for error responses.
csharp
1usingMicrosoft.AspNetCore.Mvc;23var builder = WebApplication.CreateBuilder(args);45builder.Services.AddControllers();67// Enable Problem Details for all error responses8builder.Services.AddProblemDetails();910var app = builder.Build();1112// Use Problem Details middleware13app.UseExceptionHandler();14app.UseStatusCodePages();1516app.MapControllers();17app.Run();
csharp
1[ApiController]2[Route("api/products")]3publicclassProductsController:ControllerBase4{5privatereadonlyIProductService _productService;67publicProductsController(IProductService productService)8{9 _productService = productService;10}1112[HttpGet("{id}")]13publicActionResult<Product>GetProduct(int id)14{15var product = _productService.GetById(id);1617if(product ==null)18{19returnNotFound(newProblemDetails20{21 Type ="https://api.mystore.com/errors/product-not-found",22 Title ="Product not found",23 Status =404,24 Detail =$"Product with ID {id} does not exist",25 Instance =$"/api/products/{id}"26});27}2829returnOk(product);30}31}
Response example:
json
1{2"type":"https://api.mystore.com/errors/product-not-found",3"title":"Product not found",4"status":404,5"detail":"Product with ID 123 does not exist",6"instance":"/api/products/123"7}
Extend Problem Details for validation errors:
csharp
1[ApiController]2[Route("api/orders")]3publicclassOrdersController:ControllerBase4{5[HttpPost]6publicActionResult<Order>CreateOrder([FromBody]CreateOrderRequest request)7{8var validationErrors =ValidateOrder(request);910if(validationErrors.Any())11{12var problemDetails =newValidationProblemDetails(validationErrors)13{14 Type ="https://api.mystore.com/errors/validation-failed",15 Title ="One or more validation errors occurred",16 Status =422,17 Detail ="Please correct the errors and try again",18 Instance ="/api/orders"19};2021returnUnprocessableEntity(problemDetails);22}2324var order = _orderService.Create(request);25returnCreatedAtAction(nameof(GetOrder),new{ id = order.Id }, order);26}27}
Validation error response:
json
1{2"type":"https://api.mystore.com/errors/validation-failed",3"title":"One or more validation errors occurred",4"status":422,5"detail":"Please correct the errors and try again",6"instance":"/api/orders",7"errors":{8"customerId":["Customer ID must be greater than zero"],9"items":["Order must contain at least one item"],10"items[0].quantity":["Quantity must be greater than zero"]11}12}
β Use nouns: /users, /orders
β Avoid verbs: /getUsers, /createOrder
β Be consistent: user-profiles or product-carts
β Avoid: UserProfiles, userProfiles
HTTP Methods & Status Codes:
Methods:
GET β Read
POST β Create
PUT/PATCH β Update
DELETE β Remove
Success Codes:
200: Success
201: Created
202: Accepted (async)
204: No Content
Error Codes (client):
400: Bad Request
401: Unauthorized
403: Forbidden
404: Not Found
422: Validation Failed
Error Codes (server):
500: Internal Error on Server
503: Service Unavailable
API Versioning:
URI: /api/v1/users β
Header: X-Api-Version
Media Type: application/vnd.api.v1+json
Query String: ?version=1 (avoid)
Request/Response Best Practices:
Always use JSON
Standardize error responses
Support filtering & pagination
Document with OpenAPI/Swagger
Hope you find this newsletter useful. See you next time.
Whenever you're ready, here's how I can help you:
The .NET Senior Playbook is built to:
Fast-track you from junior or mid-level to senior
Keep you growing as a senior
Help you beat any .NET interview
Covers everything: C#, ASP.NET Core, EF Core, system design β answer each question first, reveal the solution, and a test after every chapter proves it stuck. Finish, and you earn a verifiable certificate for your LinkedIn.