DEV Community

Cover image for Standardize Your ASP.NET Core API with One Line of Code
Keiz Rivas
Keiz Rivas

Posted on • Edited on

Standardize Your ASP.NET Core API with One Line of Code

Consistent routing and responses across your API — without attributes, base classes, or boilerplate.


The Problem: Inconsistent APIs

You start with a clean ASP.NET Core project.

A few sprints later:

GET  /api/UserProfile/{userId}
POST /api/V1/Products/Create
GET  /api/userOrders/GetRecent
Enter fullscreen mode Exit fullscreen mode
  • Controllers use different casing styles
  • New endpoints introduce their own conventions
  • Error responses vary depending on who implemented them

Now your API is inconsistent — and your frontend team has to guess how each endpoint behaves.

Sounds familiar?
This isn’t a framework problem.
It’s a convention problem.


The One-Line Fix

Instead of enforcing conventions manually, your API follows them automatically using AspNetConventions:

dotnet add package AspNetConventions
Enter fullscreen mode Exit fullscreen mode
builder.Services.AddControllers()
    .AddAspNetConventions(); // ← That's it
Enter fullscreen mode Exit fullscreen mode

No attributes. No base classes. No custom middleware.

From this single registration, your API gets four features automatically.


1. Route Standardization (Zero Code Changes)

Your controller stays exactly the same:

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    [HttpGet("[action]/{userId}")]
    public IActionResult GetUserDetails(int userId)
    {
        return Ok(new { userId, name = "John Doe" });
    }
}
Enter fullscreen mode Exit fullscreen mode

Before:

GET /api/Users/GetUserDetails/{userId}
Enter fullscreen mode Exit fullscreen mode

After:

GET /api/users/get-user-details/{user-id}
Enter fullscreen mode Exit fullscreen mode

What gets standardized automatically:

  • Controller names → consistent casing (kebab-case by default)
  • Action names → transformed and stripped of HTTP prefixes
  • Route parameters → consistent format

No refactoring. No attributes. No rewrites. Model binding still works — {user-id} maps back to your userId parameter transparently.

Want a different style? Just change one option:

options.Route.CaseStyle = CaseStyle.CamelCase;
Enter fullscreen mode Exit fullscreen mode

2. Response Standardization

Different endpoints often return different shapes.

That inconsistency leaks into your frontend, mobile apps, and integrations.

Before:

{ "userId": 123, "name": "John Doe" }

{ "UserId": 456, "FullName": "Jane Smith" }

"System.ArgumentNullException: Value cannot be null."
Enter fullscreen mode Exit fullscreen mode

After — every response follows the same predictable structure:

Success:

{
  "status": "success",
  "statusCode": 200,
  "message": "Request completed successfully.",
  "data": {
    "userId": 123,
    "name": "John Doe"
  },
  "metadata": {
    "requestType": "GET",
    "timestamp": "2026-04-19T14:32:00.000Z",
    "traceId": "00-ed89d1cc507c35126d6f0e933984f774-99b8b9a3feb75652-00",
    "path": "/api/user-profile/get-by-id/123"
  }
}
Enter fullscreen mode Exit fullscreen mode

Error:

{
  "status": "failure",
  "statusCode": 400,
  "type": "VALIDATION_ERROR",
  "message": "One or more validation errors occurred.",
  "errors": [
    { "Email": ["'Email' is not a valid email address."] }
  ],
  "metadata": {
    "requestType": "POST",
    "timestamp": "2026-04-19T14:33:00.000Z",
    "traceId": "00-8e5513ae9369648487c2323d9a3508aa-2a8f92c7d45d3f74-00",
    "path": "/api/user-profile/create-account"
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Exception Handling

Without centralized handling, you end up:

  • Writing try/catch in controllers
  • Returning different error formats
  • Missing logging in some paths

AspNetConventions handles exceptions globally.

So your controller actions stay clean:

[HttpGet("GetById/{userId}")]
public ActionResult GetById(int userId)
{
    var user = _users.Find(userId)
        ?? throw new KeyNotFoundException($"User {userId} was not found.");

    return Ok(user);
}
Enter fullscreen mode Exit fullscreen mode

The KeyNotFoundException is automatically caught, mapped to 404 Not Found, and returned in the standard error envelope. No try/catch. No custom middleware. No boilerplate.
The same applies to ValidationException → 400, UnauthorizedAccessException → 401, InvalidOperationException → 409, and more — all out of the box.

Need to handle your own domain exceptions?
AspNetConventions supports exception mappers.


4. JSON Serialization

Define how your API serializes JSON once — apply it everywhere.

builder.Services.AddControllers()
    .AddAspNetConventions(options =>
    {
        options.Json.ConfigureTypes = cfg =>
        {
            // Ignore a property globally
            cfg.IgnorePropertyName("InternalKey");

            // Configure a specific type
            cfg.Type<User>(type =>
            {
                type.Property(x => x.Id).Order(0);
                type.Property(x => x.Password).Ignore();
                type.Property(x => x.Alias).Name("userName");
            });
        };
    });
Enter fullscreen mode Exit fullscreen mode

You can also implement class-based configuration for your own types.


Not Just MVC

AspNetConventions works the same way with Minimal APIs and Razor Pages — same conventions, same response envelope, same exception handling:

Minimal APIs:

var api = app.UseAspNetConventions();
api.MapGet("Products/GetFeatured", () => Results.Ok(featured));
// → GET /products/get-featured
Enter fullscreen mode Exit fullscreen mode

Razor Pages:

// Pages/UserProfile/EditAddress.cshtml
// → /user-profile/edit-address/{user-id}/{address-id}
Enter fullscreen mode Exit fullscreen mode

Define your conventions once — apply them everywhere.


Customization (When You Need It)

The defaults are sensible, but you’re not locked in.

builder.Services.AddControllers()
    .AddAspNetConventions(options =>
    {
        // Routes
        options.Route.CaseStyle = CasingStyle.SnakeCase;
        options.Route.Controllers.RemoveActionPrefixes.Add("Get");
        options.Route.Controllers.ExcludeControllers.Add("LegacyController");

        // JSON
        options.Json.CaseStyle = CasingStyle.SnakeCase;
        options.Json.ScanAssemblies(typeof(UserJsonConfiguration).Assembly);

        // Responses
        options.Response.IncludeMetadata = false;
        options.Response.Pagination.DefaultPageSize = 50;
        options.Response.ErrorResponse.DefaultStatusCode = HttpStatusCode.BadRequest;

        // Exceptions
        options.ExceptionHandling.Mappers.Add(new OrderNotFoundMapper());
        options.ExceptionHandling.ExcludeException.Add(typeof(OperationCanceledException));
    });
Enter fullscreen mode Exit fullscreen mode

What You Get

Before After
Route style Mixed, manual Consistent, automatic
Response shape Varies by developer Same envelope everywhere
Exception handling try/catch per controller One global pipeline
JSON settings Scattered One config block

Try It

dotnet add package AspNetConventions
Enter fullscreen mode Exit fullscreen mode

MIT licensed. Contributions welcome.

Top comments (0)