paint-brush
.NET 8 Web API meistern: Von der Einrichtung bis zur Sicherheit – Die 50 besten Tippsvon@ssukhpinder
525 Lesungen
525 Lesungen

.NET 8 Web API meistern: Von der Einrichtung bis zur Sicherheit – Die 50 besten Tipps

von Sukhpinder Singh22m2024/04/03
Read on Terminal Reader

Zu lang; Lesen

Lernen Sie jeden Schritt zum Erstellen einer robusten API kennen, von der anfänglichen Projekteinrichtung mithilfe der .NET-CLI bis hin zur Konfiguration von Middleware, Controllern und Diensten. Entdecken Sie Best Practices für Abhängigkeitsinjektion, asynchrone Aktionen und Ausnahmebehandlung, um skalierbare, effiziente Webanwendungen zu erstellen.
featured image - .NET 8 Web API meistern: Von der Einrichtung bis zur Sicherheit – Die 50 besten Tipps
Sukhpinder Singh HackerNoon profile picture
0-item
1-item
2-item
3-item
4-item

Von der ersten Projekteinrichtung mit der .NET-CLI bis zur Konfiguration von Middleware, Controllern und Diensten lernen Sie jeden Schritt zum Aufbau einer robusten API. Entdecken Sie Best Practices für die Abhängigkeitsinjektion, asynchrone Aktionen und die Behandlung von Ausnahmen, um skalierbare, effiziente Webanwendungen zu erstellen.

1. Einrichten eines .NET 8-Web-API-Projekts

Konzept

Verwenden Sie die .NET-CLI, um ein neues Web-API-Projekt zu erstellen. Dadurch wird eine grundlegende Projektstruktur eingerichtet, einschließlich Program.cs für den Start und eines WeatherForecast-Controllers als Beispiel.

Codebeispiel

 dotnet new webapi -n MyWebApi

2. Program.cs – Minimale API-Konfiguration

Konzept

.NET 8 setzt den Trend zu minimalen APIs fort und ermöglicht Ihnen die einfache und übersichtliche Konfiguration von Diensten und Endpunkten direkt in der Datei Program.cs.

Codebeispiel

 var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", () => "Hello, World!"); app.Run();

3. Definieren eines Controllers

Konzept

Controller verarbeiten eingehende HTTP-Anfragen und antworten dem Client. Sie werden durch Erben von ControllerBase und Annotieren mit [ApiController] definiert.

Codebeispiel

 [ApiController] [Route("[controller]")] public class MyController : ControllerBase { [HttpGet] public IActionResult Get() => Ok("Hello from MyController"); }

4. Abhängigkeitsinjektion in Controllern

Konzept

Die integrierte Abhängigkeitsinjektion (DI) von .NET Core erleichtert die Verwaltung von Abhängigkeiten. Sie können Dienste über deren Konstruktoren in Ihre Controller einschleusen.

Codebeispiel

 public class MyService { public string GetMessage() => "Injected message"; } public class MyController : ControllerBase { private readonly MyService _myService; public MyController(MyService myService) { _myService = myService; } [HttpGet] public IActionResult Get() => Ok(_myService.GetMessage()); }

5. Dienste konfigurieren

Konzept

Dienste (wie Datenbankkontexte, benutzerdefinierte Dienste usw.) werden in der Datei „Program.cs“ konfiguriert, sodass sie für die Abhängigkeitsinjektion in Ihrer gesamten Anwendung verfügbar sind.

Codebeispiel

 builder.Services.AddScoped<MyService>();

6. Umgebungsbasierte Konfiguration

Konzept

.NET unterstützt umgebungsspezifische Konfigurationsdateien (appsettings.json, appsettings.Development.json usw.) und ermöglicht so unterschiedliche Einstellungen je nach Anwendungsumgebung.

Codebeispiel

 // appsettings.Development.json { "Logging": { "LogLevel": { "Default": "Debug" } } }

7. Middleware

Konzept

Middleware-Komponenten bilden eine Pipeline, die Anfragen und Antworten verarbeitet. Für übergreifende Anliegen wie Protokollierung oder Fehlerbehandlung kann benutzerdefinierte Middleware erstellt werden.

Codebeispiel

 app.Use(async (context, next) => { // Custom logic before passing to the next middleware await next(); // Custom logic after executing the next middleware });

8. Routenführung

Konzept

Das Routing in der .NET-Web-API wird durch Attributrouting auf Controllern und Aktionsmethoden erreicht. Dadurch können URLs direkt Controller-Aktionen zugeordnet werden.

Codebeispiel

 [HttpGet("myaction/{id}")] public IActionResult GetAction(int id) => Ok($"Action with ID = {id}");

9. Modellbindung

Konzept

Die Modellbindung ordnet Daten aus HTTP-Anfragen automatisch Aktionsmethodenparametern zu. Es unterstützt komplexe Typen, einschließlich JSON-Körper und Abfragezeichenfolgenparameter.

Codebeispiel

 public class MyModel { public int Id { get; set; } public string Name { get; set; } } [HttpPost] public IActionResult PostAction([FromBody] MyModel model) => Ok(model);

10. Datenvalidierung

Konzept

Datenanmerkungen können zur Validierung von Modelldaten verwendet werden. Das [ApiController]-Attribut erzwingt automatisch die Validierung und antwortet mit 400, wenn das Modell ungültig ist.

Codebeispiel

 public class MyModel { [Required] public int Id { get; set; } [StringLength(100)] public string Name { get; set; } }

11. Asynchrone Aktionen

Konzept

Asynchrone Aktionen verbessern die Skalierbarkeit, indem sie Threads freigeben, während sie auf den Abschluss von E/A-Vorgängen warten. Verwenden Sie das Schlüsselwort async und geben Sie Task oder Task<IActionResult> zurück.

Codebeispiel

 [HttpGet("{id}")] public async Task<IActionResult> GetAsync(int id) { var result = await _service.GetByIdAsync(id); return Ok(result); }

12. Ausnahmen global behandeln

Konzept

Die globale Ausnahmebehandlung ermöglicht eine zentralisierte Fehlerverarbeitung, Protokollierung und standardisierte API-Antworten auf nicht behandelte Ausnahmen.

Codebeispiel

 app.UseExceptionHandler(a => a.Run(async context => { var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>(); var exception = exceptionHandlerPathFeature.Error; // Log the exception, generate a custom response, etc. context.Response.StatusCode = 500; await context.Response.WriteAsJsonAsync(new { Error = "An unexpected error occurred" }); }));

13. API-Versionierung

Konzept

Die API-Versionierung hilft dabei, Änderungen an der API im Laufe der Zeit zu verwalten. Die .NET-Plattform unterstützt die Versionierung über Abfragezeichenfolge, URL-Pfad oder Anforderungsheader.

Codebeispiel

 builder.Services.AddApiVersioning(options => { options.AssumeDefaultVersionWhenUnspecified = true; options.DefaultApiVersion = new ApiVersion(1, 0); options.ReportApiVersions = true; });

14. Inhaltsverhandlung

Konzept

Durch die Inhaltsverhandlung kann eine API verschiedene Antwortformate basierend auf dem Accept-Header in der Anfrage bereitstellen und so Formate wie JSON, XML usw. unterstützen.

Codebeispiel

 builder.Services.AddControllers() .AddXmlDataContractSerializerFormatters();

15. Benutzerdefinierte JSON-Serialisierungseinstellungen

Konzept

Passen Sie die JSON-Antwortformatierung an, z. B. die Benennung von CamelCase oder das Ignorieren von Nullwerten, indem Sie die JSON-Serialisierungseinstellungen konfigurieren.

Codebeispiel

 builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; options.JsonSerializerOptions.IgnoreNullValues = true; });

16. CORS konfigurieren

Konzept

Cross-Origin Resource Sharing (CORS) ermöglicht den Aufruf Ihrer API von Webanwendungen, die auf verschiedenen Domänen gehostet werden. Konfigurieren Sie die CORS-Richtlinie gemäß Ihren Anforderungen.

Codebeispiel

 builder.Services.AddCors(options => { options.AddPolicy("AllowSpecificOrigin", builder => builder.WithOrigins("http://example.com")); }); app.UseCors("AllowSpecificOrigin");

17. Authentifizierung

Konzept

Sichern Sie Ihre API, indem Sie die Authentifizierung aktivieren, die die Identität von Benutzern oder Diensten überprüft, die Anfragen stellen.

Codebeispiel

 builder.Services.AddAuthentication("Bearer") .AddJwtBearer(options => { options.Authority = "https://your-auth-server"; options.Audience = "your-api"; });

18. Autorisierung

Konzept

Nach der Authentifizierung bestimmt die Autorisierung, ob ein authentifizierter Benutzer berechtigt ist, eine Aktion auszuführen oder auf eine Ressource zuzugreifen.

Codebeispiel

 [Authorize] public class SecureController : ControllerBase { // Action methods here }

19. Swagger/OpenAPI-Integration

Konzept

Swagger (OpenAPI) bietet interaktive Dokumentation für Ihre API, sodass Entwickler sie leicht verstehen und nutzen können.

Codebeispiel

 builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); app.UseSwagger(); app.UseSwaggerUI();

20. Protokollierung

Konzept

.NET Core bietet ein integriertes Protokollierungsframework, das Nachrichten an verschiedenen Ausgaben (Konsole, Debug-Fenster, externe Dienste usw.) protokollieren kann.

Codebeispiel

 logger.LogInformation("This is an informational message"); app.Use(async (context, next) => { logger.LogError("This is an error message before the next middleware"); await next.Invoke(); // Log after calling the next middleware });

21. Verwendung von Entity Framework Core

Konzept

Entity Framework Core ist ein ORM, das für den Datenzugriff in .NET-Anwendungen verwendet wird. Es ermöglicht Ihnen, Daten mithilfe stark typisierter Objekte abzufragen und zu bearbeiten.

Codebeispiel

 public class MyDbContext : DbContext { public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) {} public DbSet<MyModel> MyModels { get; set; } }

22. Migrationen im Entity Framework Core

Konzept

Mit Migrationen können Sie die Versionskontrolle auf Ihr Datenbankschema anwenden, indem Sie Änderungen in Ihren Datenmodellen verfolgen.

Codebeispiel

 dotnet ef migrations add InitialCreate dotnet ef database update

23. Repository-Muster

Konzept

Das Repository-Muster abstrahiert die Datenschicht und macht Ihre Anwendung modularer und einfacher zu warten.

Codebeispiel

 public interface IRepository<T> { Task<IEnumerable<T>> GetAllAsync(); Task<T> GetByIdAsync(int id); // Other methods... } public class MyRepository<T> : IRepository<T> where T : class { private readonly MyDbContext _context; public MyRepository(MyDbContext context) { _context = context; } // Implement methods... }

24. Unit-Tests

Konzept

Unit-Tests stellen sicher, dass Ihre Web-API ordnungsgemäß funktioniert, indem einzelne Codeeinheiten isoliert getestet werden.

Codebeispiel

 public class MyControllerTests { [Fact] public async Task Get_ReturnsExpectedValue() { // Arrange var serviceMock = new Mock<IMyService>(); serviceMock.Setup(service => service.GetAsync()).ReturnsAsync("test"); var controller = new MyController(serviceMock.Object); // Act var result = await controller.Get(); // Assert Assert.Equal("test", result.Value); } }

25. Integration mit einem Frontend

Konzept

Die .NET-Web-API kann als Backend für eine Front-End-Anwendung dienen und RESTful-Dienste bereitstellen.

Codebeispiel

 fetch('https://localhost:5001/mycontroller') .then(response => response.json()) .then(data => console.log(data));

26. Gesundheitschecks

Konzept

Gesundheitsprüfungen bieten eine Möglichkeit, den Status Ihrer Anwendung und ihrer Abhängigkeiten zu überwachen, was für Microservices-Architekturen nützlich ist.

Codebeispiel

 builder.Services.AddHealthChecks(); app.MapHealthChecks("/health");

27. Verwendung von SignalR für Echtzeitkommunikation

Konzept

SignalR ermöglicht Echtzeit-Webfunktionen, sodass serverseitiger Code asynchrone Benachrichtigungen an clientseitige Webanwendungen senden kann.

Codebeispiel

 public class MyHub : Hub { public async Task SendMessage(string user, string message) { await Clients.All.SendAsync("ReceiveMessage", user, message); } }

28. Antwort-Caching konfigurieren

Konzept

Das Zwischenspeichern von Antworten reduziert die Anzahl der Anfragen, die ein Server bearbeiten muss, indem eine Kopie der zuvor angeforderten Ressourcen gespeichert wird.

Codebeispiel

 [HttpGet("{id}")] [ResponseCache(Duration = 60)] public IActionResult GetById(int id) { // Retrieve and return your resource }

29. Statische Dateien

Konzept

Die Bereitstellung statischer Dateien (HTML, CSS, JavaScript usw.) ist für die Unterstützung von Front-End-Anwendungen mit einer .NET-Web-API unerlässlich.

Codebeispiel

 app.UseStaticFiles(); // Enable static file serving

30. Erweiterte Konfiguration und Optionsmuster

Konzept

Das Optionsmuster verwendet Klassen, um Gruppen verwandter Einstellungen darzustellen. Mit IOptions<T> können Sie überall in Ihrer Anwendung auf diese Einstellungen zugreifen.

Codebeispiel

 public class MySettings { public string Setting1 { get; set; } // Other settings } builder.Services.Configure<MySettings>(builder.Configuration.GetSection("MySettings")); public class MyService { private readonly MySettings _settings; public MyService(IOptions<MySettings> settings) { _settings = settings.Value; } // Use _settings.Setting1 }

31. Benutzerdefinierte Middleware

Konzept

Middleware ist Software, die in eine Anwendungspipeline integriert wird, um Anfragen und Antworten zu verarbeiten. Zur Ausführung bestimmter Aufgaben kann benutzerdefinierte Middleware erstellt werden.

Codebeispiel

 public class MyCustomMiddleware { private readonly RequestDelegate _next; public MyCustomMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext httpContext) { // Pre-processing logic here await _next(httpContext); // Call the next middleware in the pipeline // Post-processing logic here } } // Extension method for easy middleware registration public static class MyCustomMiddlewareExtensions { public static IApplicationBuilder UseMyCustomMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware<MyCustomMiddleware>(); } }

32. Ratenbegrenzung

Konzept

Die Ratenbegrenzung schützt Ihre API vor Überbeanspruchung, indem sie begrenzt, wie oft ein Benutzer innerhalb eines bestimmten Zeitraums Anfragen stellen kann.

Codebeispiel

 // Assume using a third-party library like AspNetCoreRateLimit builder.Services.AddInMemoryRateLimiting(); builder.Services.Configure<IpRateLimitOptions>(options => { options.GeneralRules = new List<RateLimitRule> { new RateLimitRule { Endpoint = "*", Limit = 100, Period = "1h" } }; });

33. API-Schlüsselauthentifizierung

Konzept

API-Schlüssel sind eine einfache Möglichkeit, API-Aufrufe zu authentifizieren und zu autorisieren. Sie werden entweder in der Abfragezeichenfolge oder im Header vom Client an den Server übergeben.

Codebeispiel

 public class ApiKeyMiddleware { private readonly RequestDelegate _next; private const string APIKEYNAME = "x-api-key"; public ApiKeyMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { if (!context.Request.Headers.TryGetValue(APIKEYNAME, out var extractedApiKey)) { context.Response.StatusCode = 401; await context.Response.WriteAsync("API Key was not provided."); return; } // Validate the extracted API Key here... await _next(context); } }

34. Ausgabe-Caching

Konzept

Mit dem Ausgabe-Caching können Sie die Antwort auf eine Anfrage speichern. Nachfolgende Anfragen können aus dem Cache bedient werden, was die Leistung erheblich verbessert.

Codebeispiel

 [HttpGet] [ResponseCache(Duration = 120, Location = ResponseCacheLocation.Client, NoStore = false)] public IActionResult Get() { // Your logic here }

35. Hintergrundaufgaben

Konzept

Hintergrundaufgaben ermöglichen die Ausführung von Vorgängen im Hintergrund, unabhängig von Benutzeranfragen, wie z. B. dem Senden von E-Mails oder der Verarbeitung lang laufender Aufträge.

Codebeispiel

 public class MyBackgroundService : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // Your background task logic here await Task.Delay(TimeSpan.FromHours(1), stoppingToken); } } }

36. WebSockets

Konzept

WebSockets bieten einen Vollduplex-Kommunikationskanal über eine einzige, langlebige Verbindung, ideal für Echtzeitanwendungen.

Codebeispiel

 app.UseWebSockets(); app.Use(async (context, next) => { if (context.WebSockets.IsWebSocketRequest) { WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync(); // Handle the WebSocket request here } else { await next(); } });

37. Lokalisierung anfordern

Konzept

Die Anforderungslokalisierung bietet eine Möglichkeit, Inhalte für verschiedene Kulturen und Sprachen basierend auf den Informationen der Anforderung zu lokalisieren.

Codebeispiel

 var supportedCultures = new[] { "en-US", "fr-FR" }; var localizationOptions = new RequestLocalizationOptions() .SetDefaultCulture(supportedCultures[0]) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); app.UseRequestLocalization(localizationOptions);

38. Integration mit GraphQL

Konzept

GraphQL ist eine Abfragesprache für APIs. Die Integration einer .NET-Web-API mit GraphQL ermöglicht einen effizienteren Datenabruf.

Codebeispiel

 // Assume using a library like HotChocolate builder.Services .AddGraphQLServer() .AddQueryType<Query>(); app.MapGraphQL();

39. Überwachung und Telemetrie

Konzept

Überwachung und Telemetrie umfassen das Sammeln, Analysieren und Reagieren auf Daten über die Leistung und Nutzung Ihrer Anwendung.

Codebeispiel

 // Assume using Application Insights builder.Services.AddApplicationInsightsTelemetry("YOUR_INSTRUMENTATION_KEY");

40. SignalR-Hubs und Echtzeitkommunikation

Konzept

SignalR ist eine Bibliothek, die das Hinzufügen von Echtzeit-Webfunktionen zu Apps vereinfacht. Echtzeit-Webfunktionalität ist die Möglichkeit, dass Servercode Inhalte sofort an verbundene Clients weiterleitet, ohne dass der Server darauf warten muss, dass ein Client neue Daten anfordert. SignalR eignet sich perfekt für die Entwicklung von Chat-Anwendungen, Echtzeit-Dashboards und interaktiveren Webanwendungen.

Codebeispiel

 public class ChatHub : Hub { public async Task SendMessage(string user, string message) { // Call the broadcastMessage method to update clients. await Clients.All.SendAsync("broadcastMessage", user, message); } } // Startup or Program.cs app.MapHub<ChatHub>("/chathub");

Integration in Program.cs:

 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // Other configurations... app.UseEndpoints(endpoints => { endpoints.MapHub<ChatHub>("/chathub"); }); }

41. Advanced Entity Framework Core – Beziehungen

Konzept

Entity Framework Core ermöglicht die Abbildung komplexer Beziehungen zwischen Entitäten, z. B. Eins-zu-Eins, Eins-zu-Viele und Viele-zu-Viele.

Codebeispiel

 public class Author { public int AuthorId { get; set; } public string Name { get; set; } public ICollection<Book> Books { get; set; } } public class Book { public int BookId { get; set; } public string Title { get; set; } public int AuthorId { get; set; } public Author Author { get; set; } }

42. Benutzerdefinierte Validierungsattribute

Konzept

Mit benutzerdefinierten Validierungsattributen können Sie Ihre Validierungslogik für Datenmodelle definieren und die integrierten Validierungsattribute erweitern.

Codebeispiel

 public class MyCustomValidationAttribute : ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { // Your custom validation logic here if (value is int intValue && intValue > 0) { return ValidationResult.Success; } return new ValidationResult("Value must be positive"); } } public class MyModel { [MyCustomValidationAttribute] public int MyProperty { get; set; } }

43. Erweiterte Konfigurationsszenarien

Konzept

Das Optionsmuster von .NET unterstützt komplexe Konfigurationsszenarien, einschließlich verschachtelter Objekte, Listen und Validierung.

Codebeispiel

 public class MyOptions { public MyNestedOptions Nested { get; set; } public List<string> Items { get; set; } } public class MyNestedOptions { public string Key { get; set; } } // In Program.cs or Startup.cs builder.Services.Configure<MyOptions>(builder.Configuration.GetSection("MyOptions"));

44. Leistungsüberwachung und Profilerstellung

Konzept

Durch die Überwachung und Profilierung einer Anwendung können Engpässe und Ineffizienzen identifiziert werden, was für die Optimierung der Leistung unerlässlich ist.

Codebeispiel

 app.UseMiniProfiler();

45. API-Dokumentation mit Swagger und XML-Kommentaren

Konzept

Erweitern Sie Ihre API-Dokumentation, indem Sie XML-Kommentare in Ihre Swagger-Benutzeroberfläche integrieren und so Entwicklern, die Ihre API nutzen, ein umfassenderes Erlebnis bieten.

Codebeispiel

 builder.Services.AddSwaggerGen(c => { var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); });

46. Globalisierung und Lokalisierung

Konzept

Globalisierung und Lokalisierung ermöglichen es Ihrer Anwendung, mehrere Sprachen und Kulturen zu unterstützen und sie so einem globalen Publikum zugänglich zu machen.

Codebeispiel

 builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); app.UseRequestLocalization(app.Services.GetRequiredService<IOptions<RequestLocalizationOptions>>().Value);

47. Sicherheitsheader

Konzept

Die Verbesserung der Sicherheit Ihrer Webanwendung durch das Hinzufügen verschiedener HTTP-Header kann Sie vor häufigen Angriffen und Schwachstellen schützen.

Codebeispiel

 app.UseHsts(); app.UseXContentTypeOptions(); app.UseReferrerPolicy(opts => opts.NoReferrer()); app.UseXXssProtection(options => options.EnabledWithBlockMode()); app.UseXfo(options => options.Deny());

48. Feature-Flags

Konzept

Mit Feature-Flags können Sie Funktionen Ihrer Anwendung ein- und ausschalten, ohne neuen Code bereitzustellen, was einfachere Tests und Rollouts ermöglicht.

Codebeispiel

 // Using a library like Microsoft.FeatureManagement builder.Services.AddFeatureManagement();

49. Blazor-Integration

Konzept

Mit Blazor können Sie interaktive Web-Benutzeroberflächen mit C# anstelle von JavaScript erstellen. Die Integration von Blazor mit der Web-API bietet ein nahtloses Full-Stack-Entwicklungserlebnis.

Codebeispiel

 // In a Blazor Server app @code { private IEnumerable<WeatherForecast> forecasts; protected override async Task OnInitializedAsync() { forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast"); } }

50. Erweiterte Middleware zur Antwortkomprimierung

Konzept

Die Antwortkomprimierung kann die Größe Ihrer API-Antworten reduzieren und so die Ladezeiten für Clients über langsame Netzwerke verbessern.

Codebeispiel

 builder.Services.AddResponseCompression(options => { options.Providers.Add<GzipCompressionProvider>(); options.EnableForHttps = true; }); app.UseResponseCompression();

C#-Programmierung🚀

Vielen Dank, dass Sie Teil der C#-Community sind! Bevor Sie gehen:

Wenn Sie es bis hierher geschafft haben, zeigen Sie bitte Ihre Wertschätzung mit einem Klatschen und folgen Sie dem Autor! 👏️️

Folgen Sie uns: X | LinkedIn | Dev.to | Hashnode | Newsletter | Tumblr

Besuchen Sie unsere anderen Plattformen: GitHub | Instagram | Tiktok | Quora | Daily.dev


Auch hier veröffentlicht