Isaac.

Design Stateless APIs

Build scalable APIs without server-side state.

By EMEPublished: February 20, 2025
statelessscalabilityjwthorizontal scaling

A Simple Analogy

Stateless APIs are like a grocery clerk with note-taking customers. Customers bring all info they need; clerk doesn't need to remember anyone.


Why Stateless?

  • Scalability: Add servers without coordination
  • Reliability: Server failure doesn't lose state
  • Performance: No state lookup overhead
  • Distribution: Works with load balancers
  • Simplicity: No session management

Token-Based Authentication

// Generate token
public class AuthService
{
    public string GenerateToken(User user)
    {
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        
        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Email, user.Email),
            new Claim(ClaimTypes.Role, user.Role)
        };
        
        var token = new JwtSecurityToken(
            issuer: _config["Jwt:Issuer"],
            audience: _config["Jwt:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddHours(24),
            signingCredentials: creds
        );
        
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

// Validate token
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile()
{
    var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    return Ok(new { UserId = userId });
}

Request Context

// All info in request/response
[HttpGet("orders/{id}")]
public async Task<IActionResult> GetOrder(int id)
{
    // Get user from token claims
    var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
    
    // Query database (no session lookup)
    var order = await _db.Orders
        .FirstOrDefaultAsync(o => o.Id == id && o.UserId == userId);
    
    if (order == null)
        return NotFound();
    
    return Ok(order);
}

Caching Tokens

public class TokenCacheService
{
    private readonly IDistributedCache _cache;
    
    public async Task CacheTokenAsync(string token, string userId, TimeSpan expiration)
    {
        await _cache.SetStringAsync(
            $"token:{token}",
            userId,
            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expiration }
        );
    }
    
    public async Task<bool> IsValidTokenAsync(string token)
    {
        var userId = await _cache.GetStringAsync($"token:{token}");
        return userId != null;
    }
    
    public async Task RevokeTokenAsync(string token)
    {
        await _cache.RemoveAsync($"token:{token}");
    }
}

Best Practices

  1. Tokens: Use JWT or similar standards
  2. Short-lived: Access tokens expire quickly
  3. Refresh: Use refresh tokens for long sessions
  4. Immutable: Don't rely on modifying server state
  5. Distributed: Work across multiple servers

Related Concepts

  • JWT tokens
  • OAuth 2.0
  • Load balancing
  • Microservices

Summary

Build stateless APIs using token-based authentication. All user context travels in the token, enabling horizontal scaling.