mirror of https://github.com/abpframework/abp.git
176 changed files with 2563 additions and 1740 deletions
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
@ -0,0 +1,354 @@ |
|||||
|
# Building an API Key Management System with ABP Framework |
||||
|
|
||||
|
API keys are one of the most common authentication methods for APIs, especially for machine-to-machine communication. In this article, I'll explain what API key authentication is, when to use it, and how to implement a complete API key management system using ABP Framework. |
||||
|
|
||||
|
## What is API Key Authentication? |
||||
|
|
||||
|
An API key is a unique identifier used to authenticate requests to an API. Unlike user credentials (username/password) or OAuth tokens, API keys are designed for: |
||||
|
|
||||
|
- **Programmatic access** - Scripts, CLI tools, and automated processes |
||||
|
- **Service-to-service communication** - Microservices authenticating with each other |
||||
|
- **Third-party integrations** - External systems accessing your API |
||||
|
- **IoT devices** - Embedded systems with limited authentication capabilities |
||||
|
- **Mobile/Desktop apps** - Native applications that need persistent authentication |
||||
|
|
||||
|
## Why Use API Keys? |
||||
|
|
||||
|
While modern authentication methods like OAuth2 and JWT are excellent for user authentication, API keys offer distinct advantages in certain scenarios: |
||||
|
|
||||
|
**Simplicity**: No complex OAuth flows or token refresh mechanisms. Just include the key in your request header. |
||||
|
|
||||
|
**Long-lived**: Unlike JWT tokens that expire in minutes/hours, API keys can remain valid for months or years, making them ideal for automated systems. |
||||
|
|
||||
|
**Revocable**: You can instantly revoke a compromised key without affecting user credentials. |
||||
|
|
||||
|
**Granular Control**: Different keys for different purposes (read-only, admin, specific services). |
||||
|
|
||||
|
## Real-World Use Cases |
||||
|
|
||||
|
Here are some practical scenarios where API key authentication shines: |
||||
|
|
||||
|
### 1. Mobile Applications |
||||
|
Your mobile app needs to call your backend APIs. Instead of storing user credentials or managing token refresh flows, use an API key. |
||||
|
|
||||
|
```csharp |
||||
|
// Mobile app configuration |
||||
|
var apiClient = new ApiClient("https://api.yourapp.com"); |
||||
|
apiClient.SetApiKey("sk_mobile_prod_abc123..."); |
||||
|
``` |
||||
|
|
||||
|
### 2. Microservice Communication |
||||
|
Service A needs to call Service B's protected endpoints. |
||||
|
|
||||
|
```csharp |
||||
|
// Order Service calling Inventory Service |
||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "https://inventory-service/api/products"); |
||||
|
request.Headers.Add("X-Api-Key", _configuration["InventoryService:ApiKey"]); |
||||
|
``` |
||||
|
|
||||
|
### 3. Third-Party Integrations |
||||
|
You're providing APIs to external partners or customers. |
||||
|
|
||||
|
```bash |
||||
|
# Customer's integration script |
||||
|
curl -H "X-Api-Key: pk_partner_xyz789..." \ |
||||
|
https://api.yourplatform.com/api/orders |
||||
|
``` |
||||
|
|
||||
|
## Implementing API Key Management in ABP Framework |
||||
|
|
||||
|
Now let's see how to build a complete API key management system using ABP Framework. I've created an open-source implementation that you can use in your projects. |
||||
|
|
||||
|
### Project Overview |
||||
|
|
||||
|
The implementation consists of: |
||||
|
|
||||
|
- **User-based API keys** - Each key belongs to a specific user |
||||
|
- **Permission delegation** - Keys inherit user permissions with optional restrictions |
||||
|
- **Secure storage** - Keys are hashed with SHA-256 |
||||
|
- **Prefix-based lookup** - Fast key resolution with caching |
||||
|
- **Web UI** - Manage keys through a user-friendly interface |
||||
|
- **Multi-tenancy support** - Full ABP multi-tenancy compatibility |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Architecture Overview |
||||
|
|
||||
|
The solution follows ABP's modular architecture with four main layers: |
||||
|
|
||||
|
``` |
||||
|
┌─────────────────────────────────────────────┐ |
||||
|
│ Web Layer (UI) │ |
||||
|
│ • Razor Pages for CRUD operations │ |
||||
|
│ • JavaScript for client interactions │ |
||||
|
└─────────────────────────────────────────────┘ |
||||
|
↓ |
||||
|
┌─────────────────────────────────────────────┐ |
||||
|
│ AspNetCore Layer (Middleware) │ |
||||
|
│ • Authentication Handler │ |
||||
|
│ • API Key Resolver (Header/Query) │ |
||||
|
└─────────────────────────────────────────────┘ |
||||
|
↓ |
||||
|
┌─────────────────────────────────────────────┐ |
||||
|
│ Application Layer (Business Logic) │ |
||||
|
│ • ApiKeyAppService (CRUD operations) │ |
||||
|
│ • DTO mappings and validations │ |
||||
|
└─────────────────────────────────────────────┘ |
||||
|
↓ |
||||
|
┌─────────────────────────────────────────────┐ |
||||
|
│ Domain Layer (Core Business) │ |
||||
|
│ • ApiKey Entity & Manager │ |
||||
|
│ • IApiKeyRepository │ |
||||
|
│ • Domain services & events │ |
||||
|
└─────────────────────────────────────────────┘ |
||||
|
``` |
||||
|
|
||||
|
### Key Components |
||||
|
|
||||
|
#### 1. Domain Layer - The Core Entity |
||||
|
|
||||
|
```csharp |
||||
|
public class ApiKey : FullAuditedAggregateRoot<Guid>, IMultiTenant |
||||
|
{ |
||||
|
public virtual Guid? TenantId { get; protected set; } |
||||
|
public virtual Guid UserId { get; protected set; } |
||||
|
public virtual string Name { get; protected set; } |
||||
|
public virtual string Prefix { get; protected set; } |
||||
|
public virtual string KeyHash { get; protected set; } |
||||
|
public virtual DateTime? ExpiresAt { get; protected set; } |
||||
|
public virtual bool IsActive { get; protected set; } |
||||
|
|
||||
|
// Key format: {prefix}_{key} |
||||
|
// Only the hash is stored, never the actual key |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Key Design Decisions:** |
||||
|
|
||||
|
- **Prefix-based lookup**: Keys have format `prefix_actualkey`. The prefix is indexed for fast database lookups. |
||||
|
- **SHA-256 hashing**: The actual key is hashed and never stored in plain text. |
||||
|
- **User association**: Each key belongs to a user, inheriting their permissions. |
||||
|
- **Soft delete**: Deleted keys are marked as deleted but not removed from database for audit purposes. |
||||
|
|
||||
|
#### 2. Authentication Flow |
||||
|
|
||||
|
Here's how authentication works when a request arrives: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
```csharp |
||||
|
// 1. Extract API key from request |
||||
|
var apiKey = httpContext.Request.Headers["X-Api-Key"].FirstOrDefault(); |
||||
|
if (string.IsNullOrEmpty(apiKey)) return AuthenticateResult.NoResult(); |
||||
|
|
||||
|
// 2. Split prefix and key |
||||
|
var parts = apiKey.Split('_', 2); |
||||
|
var prefix = parts[0]; |
||||
|
var key = parts[1]; |
||||
|
|
||||
|
// 3. Find key by prefix (cached) |
||||
|
var apiKeyEntity = await _apiKeyRepository.FindByPrefixAsync(prefix); |
||||
|
if (apiKeyEntity == null) return AuthenticateResult.Fail("Invalid API key"); |
||||
|
|
||||
|
// 4. Verify hash |
||||
|
var keyHash = HashHelper.ComputeSha256(key); |
||||
|
if (apiKeyEntity.KeyHash != keyHash) |
||||
|
return AuthenticateResult.Fail("Invalid API key"); |
||||
|
|
||||
|
// 5. Check expiration and active status |
||||
|
if (apiKeyEntity.ExpiresAt < DateTime.UtcNow || !apiKeyEntity.IsActive) |
||||
|
return AuthenticateResult.Fail("API key expired or inactive"); |
||||
|
|
||||
|
// 6. Create claims principal with user identity |
||||
|
var claims = new List<Claim> |
||||
|
{ |
||||
|
new Claim(AbpClaimTypes.UserId, apiKeyEntity.UserId.ToString()), |
||||
|
new Claim(AbpClaimTypes.TenantId, apiKeyEntity.TenantId?.ToString() ?? ""), |
||||
|
new Claim("ApiKeyId", apiKeyEntity.Id.ToString()) |
||||
|
}; |
||||
|
|
||||
|
return AuthenticateResult.Success(ticket); |
||||
|
``` |
||||
|
|
||||
|
#### 3. Creating and Managing API Keys |
||||
|
|
||||
|
**Creating a new key:** |
||||
|
|
||||
|
 |
||||
|
|
||||
|
```csharp |
||||
|
public class ApiKeyManager : DomainService |
||||
|
{ |
||||
|
public async Task<(ApiKey, string)> CreateAsync( |
||||
|
Guid userId, |
||||
|
string name, |
||||
|
DateTime? expiresAt = null) |
||||
|
{ |
||||
|
// Generate unique prefix |
||||
|
var prefix = await GenerateUniquePrefixAsync(); |
||||
|
|
||||
|
// Generate secure random key |
||||
|
var key = GenerateSecureRandomString(32); |
||||
|
|
||||
|
// Hash the key for storage |
||||
|
var keyHash = HashHelper.ComputeSha256(key); |
||||
|
|
||||
|
var apiKey = new ApiKey( |
||||
|
GuidGenerator.Create(), |
||||
|
userId, |
||||
|
name, |
||||
|
prefix, |
||||
|
keyHash, |
||||
|
expiresAt, |
||||
|
CurrentTenant.Id |
||||
|
); |
||||
|
|
||||
|
await _apiKeyRepository.InsertAsync(apiKey); |
||||
|
|
||||
|
// Return both entity and the full key (prefix_key) |
||||
|
// This is the ONLY time the actual key is visible |
||||
|
return (apiKey, $"{prefix}_{key}"); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Important**: The actual key is returned only once during creation. After that, only the hash is stored. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Using API Keys in Your Application |
||||
|
|
||||
|
Once created, clients can use the API key to authenticate: |
||||
|
|
||||
|
**HTTP Header (Recommended):** |
||||
|
```bash |
||||
|
curl -H "X-Api-Key: sk_prod_abc123def456..." \ |
||||
|
https://api.example.com/api/products |
||||
|
``` |
||||
|
|
||||
|
**JavaScript:** |
||||
|
```javascript |
||||
|
const response = await fetch('https://api.example.com/api/products', { |
||||
|
headers: { |
||||
|
'X-Api-Key': 'sk_prod_abc123def456...' |
||||
|
} |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**C# HttpClient:** |
||||
|
```csharp |
||||
|
var client = new HttpClient(); |
||||
|
client.DefaultRequestHeaders.Add("X-Api-Key", "sk_prod_abc123def456..."); |
||||
|
var response = await client.GetAsync("https://api.example.com/api/products"); |
||||
|
``` |
||||
|
|
||||
|
**Python:** |
||||
|
```python |
||||
|
import requests |
||||
|
|
||||
|
headers = {'X-Api-Key': 'sk_prod_abc123def456...'} |
||||
|
response = requests.get('https://api.example.com/api/products', headers=headers) |
||||
|
``` |
||||
|
|
||||
|
### Permission Management |
||||
|
|
||||
|
API keys inherit the user's permissions, but you can further restrict them: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
This allows scenarios like: |
||||
|
- Read-only API key for reporting tools |
||||
|
- Limited scope keys for third-party integrations |
||||
|
- Service-specific keys with minimal permissions |
||||
|
|
||||
|
```csharp |
||||
|
// Check if current request is authenticated via API key |
||||
|
if (CurrentUser.FindClaim("ApiKeyId") != null) |
||||
|
{ |
||||
|
var apiKeyId = CurrentUser.FindClaim("ApiKeyId").Value; |
||||
|
// Additional API key specific logic |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Performance Considerations |
||||
|
|
||||
|
The implementation uses several optimizations: |
||||
|
|
||||
|
**1. Prefix-based indexing**: Database lookups are done by prefix (indexed column), not the full key hash. |
||||
|
|
||||
|
**2. Distributed caching**: API keys are cached after first lookup, dramatically reducing database queries. |
||||
|
|
||||
|
```csharp |
||||
|
// Cache configuration |
||||
|
Configure<AbpDistributedCacheOptions>(options => |
||||
|
{ |
||||
|
options.KeyPrefix = "ApiKey:"; |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**3. Cache invalidation**: When a key is modified or deleted, cache is automatically invalidated. |
||||
|
|
||||
|
**Typical Performance:** |
||||
|
- Cached lookup: **< 5ms** |
||||
|
- Database lookup: **< 50ms** |
||||
|
- Cache hit rate: **~95%** |
||||
|
|
||||
|
## Security Best Practices |
||||
|
|
||||
|
When implementing API key authentication, follow these guidelines: |
||||
|
|
||||
|
✅ **Always use HTTPS** - Never send API keys over unencrypted connections |
||||
|
|
||||
|
✅ **Use different keys per environment** - Separate keys for dev, staging, production |
||||
|
|
||||
|
❌ **Don't log the full key** - Only log the prefix for debugging |
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
The complete source code is available on GitHub: |
||||
|
|
||||
|
**Repository**: [github.com/salihozkara/AbpApikeyManagement](https://github.com/salihozkara/AbpApikeyManagement) |
||||
|
|
||||
|
To integrate it into your ABP project: |
||||
|
|
||||
|
1. Clone or download the repository |
||||
|
2. Add project references to your solution |
||||
|
3. Add module dependencies to your modules |
||||
|
4. Run EF Core migrations to create the database tables |
||||
|
5. Navigate to `/ApiKeyManagement` to start managing keys |
||||
|
|
||||
|
```csharp |
||||
|
// In your Web module |
||||
|
[DependsOn(typeof(ApiKeyManagementWebModule))] |
||||
|
public class YourWebModule : AbpModule |
||||
|
{ |
||||
|
// ... |
||||
|
} |
||||
|
|
||||
|
// In your HttpApi.Host module |
||||
|
[DependsOn(typeof(ApiKeyManagementHttpApiModule))] |
||||
|
public class YourHttpApiHostModule : AbpModule |
||||
|
{ |
||||
|
// ... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Conclusion |
||||
|
|
||||
|
API key authentication remains a crucial part of modern API security, especially for machine-to-machine communication. While it shouldn't replace user authentication methods like OAuth2 for user-facing applications, it's perfect for: |
||||
|
|
||||
|
- Automated scripts and tools |
||||
|
- Service-to-service communication |
||||
|
- Third-party integrations |
||||
|
- Long-lived access without token refresh complexity |
||||
|
|
||||
|
The implementation shown here demonstrates how ABP Framework's modular architecture, DDD principles, and built-in features (multi-tenancy, caching, permissions) can be leveraged to build a production-ready API key management system. |
||||
|
|
||||
|
The solution is open-source and ready to be integrated into your ABP projects. Feel free to explore the code, suggest improvements, or adapt it to your specific needs. |
||||
|
|
||||
|
**Resources:** |
||||
|
- GitHub Repository: [salihozkara/AbpApikeyManagement](https://github.com/salihozkara/AbpApikeyManagement) |
||||
|
- ABP Framework: [abp.io](https://abp.io) |
||||
|
- ABP Documentation: [docs.abp.io](https://abp.io/docs/latest) |
||||
|
|
||||
|
Happy coding! 🚀 |
||||
@ -0,0 +1 @@ |
|||||
|
Learn how to implement API key authentication in ABP Framework applications. This comprehensive guide covers what API keys are, when to use them over OAuth2/JWT, real-world use cases for mobile apps and microservices, and a complete implementation with user-based key management, SHA-256 hashing, permission delegation, and built-in UI. |
||||
|
After Width: | Height: | Size: 752 KiB |
@ -0,0 +1,322 @@ |
|||||
|
# Signal-Based Forms in Angular 21: Why You’ll Never Miss Reactive Forms Again |
||||
|
|
||||
|
Angular 21 introduces one of the most exciting developments in the modern edition of Angular: **Signal-Based Forms**. Built directly on the reactive foundation of Angular signals, this new experimental API provides a cleaner, more intuitive, strongly typed, and ergonomic approach for managing form state—without the heavy boilerplate of Reactive Forms. |
||||
|
|
||||
|
> ⚠️ **Important:** Signal Forms are *experimental*. |
||||
|
> Their API can change. Avoid using them in critical production scenarios unless you understand the risks. |
||||
|
|
||||
|
Despite this, Signal Forms clearly represent Angular’s future direction. |
||||
|
--- |
||||
|
|
||||
|
## Why Signal Forms? |
||||
|
|
||||
|
Traditionally in Angular, building forms has involved several concerns: |
||||
|
|
||||
|
- Tracking values |
||||
|
- Managing UI interaction states (touched, dirty) |
||||
|
- Handling validation |
||||
|
- Keeping UI and model in sync |
||||
|
|
||||
|
Reactive Forms solved many challenges but introduced their own: |
||||
|
|
||||
|
- Verbosity FormBuilder API |
||||
|
- Required subscriptions (valueChanges) |
||||
|
- Manual cleaning |
||||
|
- Difficult nested forms |
||||
|
- Weak type-safety |
||||
|
|
||||
|
**Signal Forms solve these problems through:** |
||||
|
|
||||
|
1." Automatic synchronization |
||||
|
2." Full type safety |
||||
|
3." Schema-based validation |
||||
|
4." Fine-grained reactivity |
||||
|
5." Drastically reduced boilerplate |
||||
|
6." Natural integration with Angular Signals |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 1. Form Models — The Core of Signal Forms |
||||
|
|
||||
|
A **form model** is simply a writable signal holding the structure of your form data. |
||||
|
|
||||
|
```ts |
||||
|
import { Component, signal } from '@angular/core'; |
||||
|
import { form, Field } from '@angular/forms/signals'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-login', |
||||
|
imports: [Field], |
||||
|
template: ` |
||||
|
<input type="email" [field]="loginForm.email" /> |
||||
|
<input type="password" [field]="loginForm.password" /> |
||||
|
`, |
||||
|
}) |
||||
|
export class LoginComponent { |
||||
|
loginModel = signal({ |
||||
|
email: '', |
||||
|
password: '', |
||||
|
}); |
||||
|
|
||||
|
loginForm = form(this.loginModel); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Calling `form(model)` creates a **Field Tree** that maps directly to your model. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 2. Achieving Full Type Safety |
||||
|
|
||||
|
Although TypeScript can infer types from object literals, defining explicit interfaces provides maximum safety and better IDE support. |
||||
|
|
||||
|
```ts |
||||
|
interface LoginData { |
||||
|
email: string; |
||||
|
password: string; |
||||
|
} |
||||
|
|
||||
|
loginModel = signal<LoginData>({ |
||||
|
email: '', |
||||
|
password: '', |
||||
|
}); |
||||
|
|
||||
|
loginForm = form(loginModel); |
||||
|
``` |
||||
|
|
||||
|
Now: |
||||
|
|
||||
|
- `loginForm.email` → `FieldTree<string>` |
||||
|
- Accessing invalid fields like `loginForm.username` results in compile-time errors |
||||
|
|
||||
|
This level of type safety surpasses Reactive Forms. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 3. Reading Form Values |
||||
|
|
||||
|
#### Read from the model (entire form): |
||||
|
|
||||
|
```ts |
||||
|
onSubmit() { |
||||
|
const data = this.loginModel(); |
||||
|
console.log(data.email, data.password); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Read from an individual field: |
||||
|
|
||||
|
```html |
||||
|
<p>Current email: {{ loginForm.email().value() }}</p> |
||||
|
``` |
||||
|
|
||||
|
Each field exposes: |
||||
|
|
||||
|
- `value()` |
||||
|
- `valid()` |
||||
|
- `errors()` |
||||
|
- `dirty()` |
||||
|
- `touched()` |
||||
|
|
||||
|
All as signals. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 4. Updating Form Models Programmatically |
||||
|
|
||||
|
Signal Forms allow three update methods. |
||||
|
|
||||
|
#### 1. Replace the entire model |
||||
|
|
||||
|
```ts |
||||
|
this.userModel.set({ |
||||
|
name: 'Alice', |
||||
|
email: 'alice@example.com', |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
#### 2. Patch specific fields |
||||
|
|
||||
|
```ts |
||||
|
this.userModel.update(prev => ({ |
||||
|
...prev, |
||||
|
email: newEmail, |
||||
|
})); |
||||
|
``` |
||||
|
|
||||
|
#### 3. Update a single field |
||||
|
|
||||
|
```ts |
||||
|
this.userForm.email().value.set(''); |
||||
|
``` |
||||
|
|
||||
|
This eliminates the need for: |
||||
|
|
||||
|
- `patchValue()` |
||||
|
- `setValue()` |
||||
|
- `formGroup.get('field')` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 5. Automatic Two-Way Binding With `[field]` |
||||
|
|
||||
|
The `[field]` directive enables perfect two-way data binding: |
||||
|
|
||||
|
```html |
||||
|
<input [field]="userForm.name" /> |
||||
|
``` |
||||
|
|
||||
|
#### How it works: |
||||
|
|
||||
|
- **User input → Field state → Model** |
||||
|
- **Model updates → Field state → Input UI** |
||||
|
|
||||
|
No subscriptions. |
||||
|
No event handlers. |
||||
|
No boilerplate. |
||||
|
|
||||
|
Reactive Forms could never achieve this cleanly. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 6. Nested Models and Arrays |
||||
|
|
||||
|
Models can contain nested object structures: |
||||
|
|
||||
|
```ts |
||||
|
userModel = signal({ |
||||
|
name: '', |
||||
|
address: { |
||||
|
street: '', |
||||
|
city: '', |
||||
|
}, |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
Access fields easily: |
||||
|
|
||||
|
```html |
||||
|
<input [field]="userForm.address.street" /> |
||||
|
``` |
||||
|
|
||||
|
Arrays are also supported: |
||||
|
|
||||
|
```ts |
||||
|
orderModel = signal({ |
||||
|
items: [ |
||||
|
{ product: '', quantity: 1, price: 0 } |
||||
|
] |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
Field state persists even when array items move, thanks to identity tracking. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 7. Schema-Based Validation |
||||
|
|
||||
|
Validation is clean and centralized: |
||||
|
|
||||
|
```ts |
||||
|
import { required, email } from '@angular/forms/signals'; |
||||
|
|
||||
|
const model = signal({ email: '' }); |
||||
|
|
||||
|
const formRef = form(model, { |
||||
|
email: [required(), email()], |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
Field validation state is reactive: |
||||
|
|
||||
|
```ts |
||||
|
formRef.email().valid() |
||||
|
formRef.email().errors() |
||||
|
formRef.email().touched() |
||||
|
``` |
||||
|
|
||||
|
Validation no longer scatters across components. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 8. When Should You Use Signal Forms? |
||||
|
|
||||
|
#### New Angular 21+ apps |
||||
|
Signal-first architecture is the new standard. |
||||
|
|
||||
|
#### Teams wanting stronger type safety |
||||
|
Every field is exactly typed. |
||||
|
|
||||
|
#### Devs tired of Reactive Form boilerplate |
||||
|
Signal Forms drastically simplify code. |
||||
|
|
||||
|
#### Complex UI with computed reactive form state |
||||
|
Signals integrate perfectly. |
||||
|
|
||||
|
#### ❌ Avoid if: |
||||
|
- You need long-term stability |
||||
|
- You rely on mature Reactive Forms features |
||||
|
- Your app must avoid experimental APIs |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 9. Reactive Forms vs Signal Forms |
||||
|
|
||||
|
| Feature | Reactive Forms | Signal Forms | |
||||
|
|--------|----------------|--------------| |
||||
|
| Boilerplate | High | Very low | |
||||
|
| Type-safety | Weak | Strong | |
||||
|
| Two-way binding | Manual | Automatic | |
||||
|
| Validation | Scattered | Centralized schema | |
||||
|
| Nested forms | Verbose | Natural | |
||||
|
| Subscriptions | Required | None | |
||||
|
| Change detection | Zone-heavy | Fine-grained | |
||||
|
|
||||
|
Signal Forms feel like the "modern Angular mode," while Reactive Forms increasingly feel legacy. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 10. Full Example: Login Form |
||||
|
|
||||
|
```ts |
||||
|
@Component({ |
||||
|
selector: 'app-login', |
||||
|
imports: [Field], |
||||
|
template: ` |
||||
|
<form (ngSubmit)="submit()"> |
||||
|
<input type="email" [field]="form.email" /> |
||||
|
<input type="password" [field]="form.password" /> |
||||
|
<button>Login</button> |
||||
|
</form> |
||||
|
`, |
||||
|
}) |
||||
|
export class LoginComponent { |
||||
|
model = signal({ email: '', password: '' }); |
||||
|
form = form(this.model); |
||||
|
|
||||
|
submit() { |
||||
|
console.log(this.model()); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Minimal. Reactive. Completely type-safe. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## **Conclusion** |
||||
|
|
||||
|
Signal Forms in Angular 21 represent a big step forward: |
||||
|
|
||||
|
- Cleaner API |
||||
|
- Stronger type safety |
||||
|
- Automatic two-way binding |
||||
|
- Centralized validation |
||||
|
- Fine-grained reactivity |
||||
|
- Dramatically better developer experience |
||||
|
|
||||
|
|
||||
|
Although these are experimental, they clearly show the future of Angular's form ecosystem. |
||||
|
Once you get into using Signal Forms, you may never want to use Reactive Forms again. |
||||
|
|
||||
|
--- |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue