Backend-utveckling

Webb-API:er

Bygga RESTful API:er med ASP.NET Core: design, versionering, prestanda och felhantering.

01 · ASP.NET Core

Kodexempel

RESTful API-design

Design och implementation av RESTful API:er med ASP.NET Core.

C#
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Add services
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Configure CORS
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowSpecificOrigin",
        builder => builder
            .WithOrigins("https://example.com")
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials());
});

var app = builder.Build();

// Configure pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseCors("AllowSpecificOrigin");
app.UseAuthorization();
app.MapControllers();

app.Run();

// ProductsController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    
    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }
    
    [HttpGet]
    public async Task<ActionResult<IEnumerable<Product>>> GetProducts(
        [FromQuery] int page = 1, 
        [FromQuery] int pageSize = 10)
    {
        var products = await _productService.GetProductsAsync(page, pageSize);
        return Ok(products);
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetProduct(int id)
    {
        var product = await _productService.GetProductAsync(id);
        if (product == null)
            return NotFound();
            
        return Ok(product);
    }
    
    [HttpPost]
    public async Task<ActionResult<Product>> CreateProduct(
        [FromBody] CreateProductDto dto)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);
            
        var product = await _productService.CreateProductAsync(dto);
        return CreatedAtAction(
            nameof(GetProduct), 
            new { id = product.Id }, 
            product);
    }
    
    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateProduct(
        int id, 
        [FromBody] UpdateProductDto dto)
    {
        if (id != dto.Id)
            return BadRequest();
            
        var result = await _productService.UpdateProductAsync(dto);
        if (!result)
            return NotFound();
            
        return NoContent();
    }
    
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteProduct(int id)
    {
        var result = await _productService.DeleteProductAsync(id);
        if (!result)
            return NotFound();
            
        return NoContent();
    }
}

Komplett RESTful API med CRUD-operationer, Swagger och CORS

API-versionering

Implementera API-versionering för bakåtkompatibilitet.

C#
// Add API versioning in Program.cs
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new HeaderApiVersionReader("X-Api-Version"),
        new MediaTypeApiVersionReader("ver")
    );
});

builder.Services.AddVersionedApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

// Versioned controller
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public async Task<ActionResult<IEnumerable<ProductV1>>> GetProductsV1()
    {
        // Version 1 implementation
        return Ok(await _service.GetProductsV1Async());
    }
    
    [HttpGet]
    [MapToApiVersion("2.0")]
    public async Task<ActionResult<IEnumerable<ProductV2>>> GetProductsV2()
    {
        // Version 2 with additional fields
        return Ok(await _service.GetProductsV2Async());
    }
}

// Configure Swagger for multiple versions
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo 
    { 
        Title = "My API", 
        Version = "v1" 
    });
    options.SwaggerDoc("v2", new OpenApiInfo 
    { 
        Title = "My API", 
        Version = "v2" 
    });
});

API-versionering med URL, header och media type support

Response caching & komprimering

Optimera API-prestanda med caching och komprimering.

C#
// Configure response caching and compression
builder.Services.AddResponseCaching();
builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
    options.MimeTypes = ResponseCompressionDefaults.MimeTypes
        .Concat(new[] { "application/json" });
});

// Configure caching policies
builder.Services.AddControllers(options =>
{
    options.CacheProfiles.Add("Default30",
        new CacheProfile()
        {
            Duration = 30,
            Location = ResponseCacheLocation.Any,
            NoStore = false
        });
    options.CacheProfiles.Add("NoCache",
        new CacheProfile()
        {
            NoStore = true,
            Location = ResponseCacheLocation.None
        });
});

// Use in middleware pipeline
app.UseResponseCompression();
app.UseResponseCaching();

// Controller with caching
[ApiController]
[Route("api/[controller]")]
public class CatalogController : ControllerBase
{
    private readonly IMemoryCache _cache;
    private readonly ICatalogService _service;
    
    public CatalogController(IMemoryCache cache, ICatalogService service)
    {
        _cache = cache;
        _service = service;
    }
    
    [HttpGet("products")]
    [ResponseCache(CacheProfileName = "Default30")]
    public async Task<IActionResult> GetProducts()
    {
        // Response caching header
        Response.Headers.Add("X-Cache", "HIT");
        
        var products = await _service.GetProductsAsync();
        return Ok(products);
    }
    
    [HttpGet("categories")]
    public async Task<IActionResult> GetCategories()
    {
        const string cacheKey = "categories_all";
        
        // Try get from memory cache
        if (_cache.TryGetValue(cacheKey, out List<Category> categories))
        {
            Response.Headers.Add("X-Cache", "HIT");
            return Ok(categories);
        }
        
        // Get from database and cache
        categories = await _service.GetCategoriesAsync();
        
        var cacheOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromMinutes(5))
            .SetAbsoluteExpiration(TimeSpan.FromHours(1));
            
        _cache.Set(cacheKey, categories, cacheOptions);
        
        Response.Headers.Add("X-Cache", "MISS");
        return Ok(categories);
    }
}

Response caching och komprimering för bättre prestanda

Felhantering

Robust felhantering med konsekvent error response-format.

C#
// Global error handling middleware
public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;
    
    public GlobalExceptionMiddleware(
        RequestDelegate next, 
        ILogger<GlobalExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred");
            await HandleExceptionAsync(context, ex);
        }
    }
    
    private static async Task HandleExceptionAsync(
        HttpContext context, 
        Exception exception)
    {
        context.Response.ContentType = "application/json";
        
        var response = new ApiErrorResponse();
        
        switch (exception)
        {
            case NotFoundException:
                response.StatusCode = StatusCodes.Status404NotFound;
                response.Message = exception.Message;
                break;
                
            case ValidationException validationEx:
                response.StatusCode = StatusCodes.Status400BadRequest;
                response.Message = "Validation failed";
                response.Errors = validationEx.Errors;
                break;
                
            case UnauthorizedException:
                response.StatusCode = StatusCodes.Status401Unauthorized;
                response.Message = "Unauthorized access";
                break;
                
            default:
                response.StatusCode = StatusCodes.Status500InternalServerError;
                response.Message = "An error occurred while processing your request";
                break;
        }
        
        context.Response.StatusCode = response.StatusCode;
        
        var jsonResponse = JsonSerializer.Serialize(response, new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        });
        
        await context.Response.WriteAsync(jsonResponse);
    }
}

// Error response model
public class ApiErrorResponse
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public string Details { get; set; }
    public Dictionary<string, string[]> Errors { get; set; }
    public string TraceId { get; set; }
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}

// Custom exceptions
public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}

public class ValidationException : Exception
{
    public Dictionary<string, string[]> Errors { get; }
    
    public ValidationException(Dictionary<string, string[]> errors)
    {
        Errors = errors;
    }
}

// Register middleware
app.UseMiddleware<GlobalExceptionMiddleware>();

// Problem Details for standard error responses
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Extensions["traceId"] = 
            Activity.Current?.Id ?? context.HttpContext.TraceIdentifier;
            
        context.ProblemDetails.Extensions["timestamp"] = 
            DateTime.UtcNow;
    };
});

Centraliserad felhantering med konsekvent error response-format

02 · Riktlinjer

Best practices

API-design

  • Använd RESTful principer konsekvent
  • Versionera API:er från början
  • Dokumentera med OpenAPI/Swagger
  • Använd meningsfulla HTTP-statuskoder

Säkerhet

  • Implementera autentisering och auktorisering
  • Använd HTTPS för all kommunikation
  • Validera och sanera all input
  • Implementera rate limiting

Prestanda

  • Implementera caching strategiskt
  • Använd paginering för stora dataset
  • Komprimera responses
  • Optimera databas-queries

Underhåll

  • Logga alla requests och errors
  • Använd health checks
  • Implementera monitoring
  • Håll dependencies uppdaterade

03 · Referens

HTTP-statuskoder

KodBetydelse
200 OKLyckad request
201 CreatedResurs skapad
204 No ContentLyckad, inget innehåll
400 Bad RequestOgiltig request
401 UnauthorizedAutentisering krävs
403 ForbiddenÅtkomst nekad
404 Not FoundResurs ej hittad
500 Internal Server ErrorServerfel
502 Bad GatewayGateway-fel
503 Service UnavailableTjänst ej tillgänglig

04 · Verktyg

Testning & dokumentation

Testverktyg

Postman
API-testning och dokumentation
Insomnia
REST- och GraphQL-klient
cURL
HTTP-klient i kommandoraden
REST Client
VS Code-tillägg

Dokumentation

Swagger/OpenAPI
API-specifikation
ReDoc
API-dokumentation
API Blueprint
Design-first approach
Postman Collections
Delbar dokumentation