Building a Modern Development Platform: TypeSpec for Contract-First API Development π
Introduction π
In our Aspire local development post, we built a weather application with a .NET API and React frontend. While the application works perfectly, we noticed something: the frontend and backend teams had to coordinate manually on the API contract. The React TypeScript interfaces had to match the C# models exactly, and any changes required careful synchronization.
This is where TypeSpec shines. Instead of defining APIs in code and hoping both teams stay in sync, TypeSpec lets us define the contract first, then generate both the OpenAPI specification and the client/server code from a single source of truth.
What is TypeSpec? π€
TypeSpec is Microsoftβs modern API description language designed to overcome the limitations of writing OpenAPI specifications directly. Think of it as βTypeScript for API contractsβ - it provides:
A Clean, Readable Syntax
model WeatherForecast {
id: string;
date: plainDate;
temperatureC: int32;
temperatureF: int32;
summary: string;
location: string;
}
@route("/weather")
namespace WeatherAPI {
@get
op getForecast(): WeatherForecast[];
}
Instead of this verbose OpenAPI YAML:
paths:
/weather:
get:
operationId: getForecast
responses:
'200':
description: Success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/WeatherForecast'
components:
schemas:
WeatherForecast:
type: object
properties:
id:
type: string
date:
type: string
format: date
temperatureC:
type: integer
format: int32
# ... and so on
Key Characteristics:
- Type-Safe: Strong typing prevents common API contract errors
- Composable: Reusable models, mixins, and decorators
- Extensible: Plugin system for custom behaviors
- Multi-Target: Generate OpenAPI, JSON Schema, client SDKs, and more
- Tooling: Rich VS Code extension with IntelliSense and validation
Why Would You Use TypeSpec? π‘
1. Contract-First Development π
Traditional development often follows this painful pattern:
- Backend team implements API endpoints
- Frontend team reverse-engineers the contract from API responses
- Changes break both teams
- Manual coordination and documentation drift
With TypeSpec:
- Define the contract in TypeSpec
- Generate OpenAPI spec automatically
- Generate client SDKs for frontend
- Generate server scaffolding for backend
- Both teams work from the same source of truth
2. Superior Developer Experience π¨βπ»
Type Safety Everywhere
- Compile-time validation of your API contracts
- IntelliSense and autocomplete in VS Code
- Catch breaking changes before they reach production
Clean, Maintainable Specifications
- No more hand-editing massive OpenAPI YAML files
- Reusable components and mixins
- Version control friendly (readable diffs)
Rapid Prototyping
- Define APIs quickly without implementation overhead
- Generate mock servers for frontend development
- Validate designs before committing to implementation
3. Multi-Platform Code Generation ποΈ
From one TypeSpec definition, generate:
- OpenAPI 3.0/3.1 specifications
- Client SDKs (TypeScript, C#, Python, Java, Go)
- Server stubs (.NET, Node.js, Java)
- Documentation (interactive API docs)
- Mock servers for testing and development
4. Enterprise-Grade Features π’
Versioning Support
@versioned(Versions)
namespace WeatherAPI {
enum Versions {
v1: "1.0",
v2: "2.0",
}
}
Authentication & Security
@useAuth(BearerAuth)
@route("/weather")
op getForecast(): WeatherForecast[];
Advanced Validation
model CreateWeatherRequest {
@minLength(1)
@maxLength(100)
location: string;
@minimum(-50)
@maximum(60)
temperatureC: int32;
}
5. Real-World Problem Solving π οΈ
Problem: Your weather API needs to support both Celsius and Fahrenheit, with validation TypeSpec Solution:
enum TemperatureUnit {
Celsius: "C",
Fahrenheit: "F",
}
model TemperatureReading {
@minimum(-459.67) // Absolute zero in Fahrenheit
value: float32;
unit: TemperatureUnit;
}
Problem: Different clients need different response formats TypeSpec Solution:
@discriminator("kind")
union WeatherResponse {
detailed: DetailedWeather,
summary: WeatherSummary,
}
Problem: API needs to evolve without breaking existing clients TypeSpec Solution:
@added(Versions.v2)
model WeatherForecast {
// ... existing properties
@added(Versions.v2)
humidity?: int32;
@added(Versions.v2)
windSpeed?: float32;
}
6. Integration with Modern Tools π§
TypeSpec integrates seamlessly with:
- Microsoft Kiota - Generate strongly-typed HTTP clients
- Azure API Management - Import generated OpenAPI specifications
- OpenAPI tooling - Works with existing OpenAPI ecosystem
- CI/CD pipelines - Automate spec validation and code generation
- API gateways - Deploy contracts to Kong, Envoy, etc.
The Alternative: Manual API Management π°
Without TypeSpec, API development typically follows one of two problematic approaches:
Approach 1: Hand-Written OpenAPI Specifications π
The Process:
- Write OpenAPI YAML/JSON files manually
- Struggle with verbose, repetitive syntax
- Manually keep specifications in sync with code
The Problems:
- Documentation Drift - Hand-written specs quickly become outdated
- Error-Prone - Typos in massive YAML files break integrations
- Maintenance Nightmare - Complex specifications become unwieldy
- No Validation - Missing validation rules and inconsistent patterns
Approach 2: Code-First with Manual Export/Import π
The Process:
- Write API implementation first (ASP.NET Core, FastAPI, etc.)
- Export OpenAPI spec from running application
- Manually import spec into API management tools
- Hope frontend teams can work with whatβs generated
The Problems:
- Implementation Lock-in - API design driven by code constraints
- Export/Import Friction - Manual steps that teams forget or skip
- Generated Specs - Often incomplete or poorly documented
- Coordination Overhead - Constant communication between teams about API changes
- Late Discovery - Integration issues found after implementation
Common Pain Points Across Both Approaches π©
Team Coordination
- Frontend and backend models diverge over time
- Manual client SDK updates required for every change
- Time wasted on preventable integration issues
Quality Issues
- Missing validation rules in specifications
- Inconsistent error handling across endpoints
- Poor documentation that doesnβt match reality
Scaling Challenges
- Multiple APIs with different standards and patterns
- No reusable components across teams
- Difficult to maintain consistency as organization grows
TypeSpec in Action: A Quick Example π―
Letβs see how TypeSpec would improve our weather application from the Aspire series:
Traditional Approach (what we had):
// Backend C# model
public record WeatherForecast(Guid Id, DateOnly Date, int TemperatureC, string? Summary, string Location);
// Frontend TypeScript interface (manually created)
export interface WeatherForecast {
id: string;
date: string;
temperatureC: number;
temperatureF: number;
summary: string;
location: string;
}
TypeSpec Approach:
model WeatherForecast {
@format("uuid")
id: string;
@format("date")
date: plainDate;
@minimum(-50)
@maximum(60)
temperatureC: int32;
temperatureF: int32;
@minLength(1)
@maxLength(100)
summary: string;
@minLength(1)
@maxLength(50)
location: string;
}
@route("/weather")
namespace WeatherAPI {
@get
op getForecast(): {
@statusCode statusCode: 200;
@body forecasts: WeatherForecast[];
};
@post
op createForecast(@body forecast: WeatherForecast): {
@statusCode statusCode: 201;
@body created: WeatherForecast;
};
}
From this single definition:
- Generate OpenAPI spec
- Generate TypeScript types for React
- Generate C# models for .NET
- Generate validation attributes
- Generate API client code
- Generate server stubs
Installing TypeSpec Tooling π οΈ
Letβs get TypeSpec set up in our development environment. Weβll need Node.js (which we already have in our Aspire dev container) and the TypeSpec tools.
Prerequisites π¦
From our previous Aspire tutorial, we already have:
- π³ Dev container with Node.js installed
- π€οΈ Our weather application running
- π§ VS Code with proper extensions
Install TypeSpec CLI and Compiler π»
# Install TypeSpec globally
npm install -g @typespec/compiler
# Verify installation
tsp --version
What this installs:
- TypeSpec Compiler (
tsp) - Core compiler for processing.tspfiles - Standard Library - Built-in types like
string,int32,utcDateTime - CLI Tools - Commands for project initialization, compilation, and scaffolding
Verify Everything Works:
# Check available commands
tsp --help
# See installed version and location
tsp --version
which tsp
Install VS Code Extension π¨
The TypeSpec VS Code extension provides syntax highlighting, IntelliSense, and real-time validation:
- Open VS Code
- Go to Extensions (Ctrl+Shift+X)
- Search for βTypeSpecβ
- Install the official Microsoft TypeSpec extension
Adding TypeSpec to Existing Project π
Letβs add TypeSpec to our existing weather application from the Aspire series. Weβll create a new TypeSpec project that will define our API contracts.
Step 1: Initialize TypeSpec Project π
Navigate to your weather app repository and create a TypeSpec project:
# From your project root (where you have src/ folder)
mkdir WeatherApp.Typespec
cd WeatherApp.Typespec
# Initialize a new TypeSpec project
tsp init
The TypeSpec CLI will guide you through setup:
- Template: Choose βGeneric Rest APIβ
- Package name:
weather-api-contracts - Description: βAPI contracts for weather applicationβ
This creates:
WeatherApp.Typespec/
βββ package.json
βββ tspconfig.yaml
βββ main.tsp
βββ node_modules/
Step 2: Configure TypeSpec Project π§
Update tspconfig.yaml to configure our emitters (code generators):
emit:
- "@typespec/openapi3"
options:
"@typespec/openapi3":
emitter-output-dir: "{output-dir}/../../generated/openapi"
This configuration:
emit- Specifies which emitters to run (OpenAPI 3 in this case)emitter-output-dir- Uses relative path to output tosrc/generated/openapi{output-dir}- TypeSpecβs placeholder for the default output directory (tsp-output)- The
../../navigates up fromWeatherApp.Typespec/tsp-outputto the project root, then intosrc/generated/openapi
Install the required emitters:
npm install @typespec/openapi3
Defining Our Weather API Contract π€οΈ
Replace the content of main.tsp with our complete weather API definition:
import "@typespec/http";
using Http;
@service({
title: "Weather API",
})
@server("http://localhost:5008", "Development server")
namespace WeatherAPI;
@doc("Represents a weather forecast for a specific location and date")
model WeatherForecast {
@doc("Unique identifier for the weather forecast")
id: string;
@doc("Date of the weather forecast")
date: plainDate;
@doc("Temperature in Celsius")
temperatureC: int32;
@doc("Temperature in Fahrenheit (calculated)")
temperatureF: int32;
@doc("Weather summary description")
summary?: string;
@doc("Geographic location of the forecast")
location: string;
}
@doc("Standard error response")
@error
model ErrorResponse {
@doc("Error code")
code: string;
@doc("Human-readable error message")
message: string;
@doc("Additional error details")
details?: string;
@doc("Timestamp when error occurred")
timestamp: utcDateTime;
@doc("Request ID for tracking")
requestId?: string;
}
@doc("Bad request error")
model BadRequestError extends ErrorResponse {
code: "BAD_REQUEST";
}
@doc("List of weather forecasts")
model WeatherForecastList {
@doc("Array of weather forecast items")
items: WeatherForecast[];
}
@route("/weatherforecast")
@tag("Weather")
interface WeatherForecasts {
/** Get all weather forecasts */
@get
op listForecasts(): {
@statusCode statusCode: 200;
@body forecasts: WeatherForecast[];
} | {
@statusCode statusCode: 500;
@body error: ErrorResponse;
};
/** Add a new weather forecast */
@post
op addForecast(
@body forecast: WeatherForecast
): {
@statusCode statusCode: 201;
@body createdForecast: WeatherForecast;
} | {
@statusCode statusCode: 400;
@body error: BadRequestError;
} | {
@statusCode statusCode: 500;
@body error: ErrorResponse;
};
}
Key Features of This TypeSpec Definition:
Server Configuration
@server("http://localhost:5008", "Development server")- Defines the base URL for the API- This will be included in the generated OpenAPI specification
- Makes it easy to test the API with tools like Swagger UI
Clean Error Hierarchy
ErrorResponse- Base error model with common fields (code, message, details, timestamp, requestId)BadRequestError- Extends base with specific error code constant- Easily extensible for additional error types (NotFoundError, ValidationError, etc.)
Explicit Response Structure
- Uses
@statusCodeand@bodydecorators for clear response definitions - Union types (
|) to define success and error responses - Makes the API contract explicit and unambiguous
Interface-Based Operations
interface WeatherForecastsgroups related operations- Cleaner than namespace-based operations
- Better IntelliSense and tooling support in VS Code
Creating OpenAPI File with Emitter π
Now letβs generate the OpenAPI specification from our TypeSpec definition:
# From the WeatherApp.Typespec directory
tsp compile .
This generates:
src/
βββ generated/
βββ openapi/
βββ openapi.yaml
The generated openapi.yaml will contain a complete OpenAPI 3.0 specification with:
- All endpoints with proper HTTP methods
- Request/response schemas
- Validation rules
- Error responses
- Documentation from our
@docdecorators
Verify Generated OpenAPI
Letβs check what we generated:
# View the generated OpenAPI spec
cat src/generated/openapi/openapi.yaml
Youβll see a fully-formed OpenAPI specification thatβs much cleaner and more comprehensive than what we could write by hand!
Adding Swagger to AppHost π
Letβs integrate Swagger UI into our Aspire application so we can view and test our API specification.
Update WeatherApp.AppHost/AppHost.cs to include a Swagger UI container:
var builder = DistributedApplication.CreateBuilder(args);
#pragma warning disable ASPIRECOSMOSDB001
var cosmos = builder.AddAzureCosmosDB("cosmos").RunAsPreviewEmulator(
emulator =>
{
emulator.WithDataExplorer();
});
#pragma warning restore ASPIRECOSMOSDB001
var database = cosmos.AddCosmosDatabase("WeatherDB");
var container = database.AddContainer("WeatherData", "/location");
var api = builder.AddProject<Projects.WeatherApp_Api>("weatherapi")
.WithReference(container);
var seeder = builder.AddProject<Projects.WeatherApp_Seed>("weatherseeder")
.WithReference(container)
.WithExplicitStart();
var swagger = builder.AddContainer("swagger-ui", "swaggerapi/swagger-ui")
.WithBindMount("../generated/openapi/openapi.yaml", "/usr/share/nginx/html/openapi.yaml")
.WithEnvironment("SWAGGER_JSON_URL", "/openapi.yaml")
.WithHttpEndpoint(targetPort: 8080, name: "swagger");
var frontend = builder.AddViteApp("frontend", "../WeatherApp.Web")
.WithNpmPackageInstallation()
.WithReference(api)
.WithEnvironment("BROWSER", "false")
.WithExternalHttpEndpoints();
builder.Build().Run();
Key Configuration Details:
Swagger UI Container
- Uses the official
swaggerapi/swagger-uiDocker image WithBindMount- Mounts the generated OpenAPI file directly to the Swagger UI containerWithEnvironment("SWAGGER_JSON_URL", "/openapi.yaml")- Tells Swagger UI where to find the specWithHttpEndpoint(targetPort: 8080)- Exposes Swagger UI on port 8080
Cosmos DB Partition Key
- Updated to use
/locationas the partition key (matching our WeatherForecast model) - Previously was
/idwhich wasnβt optimal for querying by location
Service Ordering
- API and seeder are defined before Swagger
- Ensures the OpenAPI file is generated before Swagger tries to load it
- Swagger appears in the Aspire dashboard with the other services
Creating Controllers with Emitter ποΈ
Now letβs create a TypeSpec emitter that generates .NET controllers from our specification.
Step 1: Install ASP.NET Core Emitter π¦
# From WeatherApp.Typespec directory
npm install @typespec/http-server-csharp
Step 2: Update TypeSpec Configuration βοΈ
Update tspconfig.yaml to include the C# emitter:
emit:
- "@typespec/openapi3"
- "@typespec/http-server-csharp"
options:
"@typespec/openapi3":
emitter-output-dir: "{output-dir}/../../generated/openapi"
"@typespec/http-server-csharp":
emitter-output-dir: "{output-dir}/../../WeatherApp.Api"
emit-mocks: none
Key Configuration Options:
emitter-output-dir: "{output-dir}/../../WeatherApp.Api"- Generates C# code directly into the API projectemit-mocks: none- Disables mock generation, we only want the models and interfaces- This approach integrates generated code directly into your existing project structure
Step 3: Generate Controllers π€
# Regenerate with new emitter
tsp compile .
This creates files directly in your API project:
src/
βββ WeatherApp.Api/
β βββ generated/ # TypeSpec-generated code
β β βββ Models/
β β β βββ WeatherForecast.cs
β β β βββ ErrorResponse.cs
β β β βββ BadRequestError.cs
β β βββ ... (other generated files)
β βββ ... (other API files)
βββ generated/
βββ openapi/
βββ openapi.yaml
Step 4: Clean Up Data Project π§Ή
Now that weβre using TypeSpec-generated code, we can remove some files from the Data project that are no longer needed:
Remove these files from WeatherApp.Data/:
ServiceExtensions.cs- Service registration is now handled by generated codeWeatherService.cs/IWeatherService- Business logic will use generated models directly
The generated C# server code from TypeSpec includes:
- Models that match your TypeSpec definitions
- Interfaces for operations
- Proper serialization attributes
Keep in WeatherApp.Data/:
WeatherContext.cs- Entity Framework DbContext for database accessWeatherForecast.cs- Can be replaced with the generated model or kept for EF-specific configurations
Step 5: Update API to Use Generated Models π¦
Update your WeatherApp.Api/Program.cs to use the generated models:
using WeatherApi;
using WeatherApp.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.AddServiceDefaults();
// Add Entity Framework with Cosmos DB - connection is injected automatically by Aspire
builder.AddCosmosDbContext<WeatherContext>("WeatherData");
// Register weather services from the Data project
builder.Services.AddScoped<IWeatherForecasts, WeatherService>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
Key Points:
IWeatherForecasts- Interface generated from TypeSpec operationsWeatherService- Your implementation of the generated interfaceAddOpenApi()- Uses built-in .NET OpenAPI support instead of Swashbuckle- Simplified - No manual Swagger configuration, leveraging Aspire and generated code
Step 6: Implement the Generated Interface π
Create WeatherApp.Api/WeatherService.cs to implement the TypeSpec-generated interface:
using System.Text.Json.Nodes;
using WeatherApi;
using WeatherApp.Data;
using Microsoft.EntityFrameworkCore;
public class WeatherService : IWeatherForecasts
{
private readonly WeatherContext _context;
public WeatherService(WeatherContext context)
{
_context = context;
}
public async Task<JsonNode> AddForecastAsync(WeatherApi.WeatherForecast body)
{
var entity = new WeatherApp.Data.WeatherForecast(
Guid.NewGuid(),
DateOnly.FromDateTime(body.Date),
body.TemperatureC,
body.Summary,
body.Location
);
_context.WeatherForecasts.Add(entity);
await _context.SaveChangesAsync();
return System.Text.Json.JsonSerializer.SerializeToNode(entity) ?? new JsonObject();
}
public async Task<JsonNode> ListForecastsAsync()
{
var forecasts = await _context.WeatherForecasts.ToListAsync();
var json = System.Text.Json.JsonSerializer.Serialize(forecasts);
var node = JsonNode.Parse(json);
return node ?? new JsonArray();
}
}
Implementation Details:
IWeatherForecasts- Generated interface from TypeSpec withListForecastsAsync()andAddForecastAsync()methodsWeatherApi.WeatherForecast- Generated model from TypeSpec used for API contractWeatherApp.Data.WeatherForecast- Entity Framework model for database persistence- Type Mapping - Converts between API models and database entities
JsonNodeReturns - Generated interface usesJsonNodefor flexible response handling
Why Two Models?
- API Model (
WeatherApi.WeatherForecast) - Generated from TypeSpec, matches API contract exactly - Database Model (
WeatherApp.Data.WeatherForecast) - Entity Framework entity with database-specific attributes - This separation allows the API contract to evolve independently from database schema
Step 7: Update Seed Project π±
The seed project can use Entity Framework directly without needing the TypeSpec-generated service layer. Update WeatherApp.Seed/Program.cs:
using WeatherApp.Data;
using WeatherApp.Seed;
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
// Add Entity Framework with Cosmos DB
builder.AddCosmosDbContext<WeatherContext>("WeatherData");
// Register the worker service that seeds data
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
Simplified Seed Project:
- Direct EF Access - Worker service injects
WeatherContextdirectly - No API Layer - Seed project doesnβt need the API contract or generated services
- Database-Only - Works with
WeatherApp.Data.WeatherForecastentities directly - Separation of Concerns - TypeSpec contract is for API consumers, not internal tooling
Update WeatherApp.Seed/Worker.cs to use Entity Framework directly:
using Microsoft.EntityFrameworkCore;
using WeatherApp.Data;
namespace WeatherApp.Seed;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
public Worker(ILogger<Worker> logger, IServiceScopeFactory serviceScopeFactory,
IHostApplicationLifetime hostApplicationLifetime)
{
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
_hostApplicationLifetime = hostApplicationLifetime;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Weather Seeder starting at: {time}", DateTimeOffset.Now);
using var scope = _serviceScopeFactory.CreateScope();
var weatherContext = scope.ServiceProvider.GetRequiredService<WeatherContext>();
// Check if data already exists
var firstForecast = await weatherContext.WeatherForecasts
.FirstOrDefaultAsync(stoppingToken);
if (firstForecast != null)
{
_logger.LogInformation("Weather data already exists. Skipping seed.");
_hostApplicationLifetime.StopApplication();
return;
}
// Seed fake weather data
_logger.LogInformation("Seeding weather data...");
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm",
"Balmy", "Hot", "Sweltering", "Scorching"
};
var cities = new[]
{
"New York, USA", "London, UK", "Tokyo, Japan", "Sydney, Australia",
"Paris, France", "Berlin, Germany", "Toronto, Canada", "Mumbai, India",
"SΓ£o Paulo, Brazil", "Cairo, Egypt", "Moscow, Russia", "Beijing, China",
"Mexico City, Mexico", "Lagos, Nigeria", "Bangkok, Thailand", "Dubai, UAE",
"Singapore", "Buenos Aires, Argentina", "Stockholm, Sweden",
"Amsterdam, Netherlands"
};
var forecasts = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
Guid.NewGuid(),
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)],
cities[Random.Shared.Next(cities.Length)]
))
.ToArray();
// Add all forecasts to the context
await weatherContext.WeatherForecasts.AddRangeAsync(forecasts, stoppingToken);
await weatherContext.SaveChangesAsync(stoppingToken);
foreach (var forecast in forecasts)
{
_logger.LogInformation("Added forecast: {Date} - {Location} - {Summary} - {TempC}Β°C",
forecast.Date, forecast.Location, forecast.Summary, forecast.TemperatureC);
}
_logger.LogInformation("Weather Seeder completed seeding {Count} forecasts at: {time}",
forecasts.Length, DateTimeOffset.Now);
// Stop the application after seeding is complete
_hostApplicationLifetime.StopApplication();
}
}
Worker Service Implementation:
- Direct DbContext Injection - Uses
IServiceScopeFactoryto create a scope and getWeatherContext - Idempotent Seeding - Checks if data exists before seeding to avoid duplicates
- Database Entities - Works directly with
WeatherApp.Data.WeatherForecastrecord - Auto-Shutdown - Stops the application after seeding completes (or if data exists)
- Cosmos DB Compatible - Uses
FirstOrDefaultAsync()instead ofAny()for better Cosmos performance
This demonstrates a key architectural benefit: internal tools (like seeders) can work directly with the database layer, while external consumers use the TypeSpec-defined API contract.
Testing the Complete Setup π§ͺ
Step 1: Generate and Run π
# Generate all contracts and code
cd WeatherApp.Typespec
tsp compile .
# Run the Aspire application
cd ..
aspire run
Step 2: View Results ποΈ
Your Aspire dashboard now shows:
- Weather API - Your .NET API with generated controllers
- Frontend - React app consuming the API
- Swagger UI - Interactive API documentation at http://localhost:8080
- Cosmos DB - Database with seeded data
Step 3: Test the API β
Visit the Swagger UI to:
- View complete API documentation
- Test endpoints interactively
- See validation rules in action
- Verify error responses
GitHub Repository & Resources π
Complete Example Repository
All code from this tutorial is available at: github.com/two4suited/blog-platform-typespec
Branch: aspire-tools-typespec
The repository includes:
- Complete TypeSpec definitions
- Generated OpenAPI specifications
- .NET API with generated models
- React frontend with TypeScript types
- Aspire orchestration with Swagger UI
- CI/CD pipeline examples
Folder Structure
blog-platform-typespec/
βββ WeatherApp.Typespec/ # TypeSpec definitions
β βββ main.tsp
β βββ tspconfig.yaml
β βββ package.json
βββ src/ # Aspire application
β βββ generated/ # Generated OpenAPI specs
β β βββ openapi/
β βββ WeatherApp.Api/ # API project
β β βββ generated/ # TypeSpec-generated models
β β βββ Models/
β βββ WeatherApp.Web/
β βββ WeatherApp.AppHost/
βββ README.md
Additional Resources π
Official Documentation
- TypeSpec Documentation - Complete language reference and guides
- TypeSpec GitHub - Source code and issues
- TypeSpec Playground - Try TypeSpec in your browser
- TypeSpec VS Code Extension - Official editor support
Emitters and Tools
- OpenAPI 3 Emitter - Generate OpenAPI specs
- HTTP Server C# Emitter - Generate .NET controllers
- TypeScript Client Emitter - Generate TypeScript clients
- JSON Schema Emitter - Generate JSON schemas
Community Resources
- TypeSpec Samples - Example TypeSpec definitions
- TypeSpec Discord - Community discussion and support
- Azure REST API Specs - Real-world TypeSpec examples from Azure
- Microsoft Graph TypeSpec - Large-scale TypeSpec implementation
Learning Path
- Start Here: TypeSpec Getting Started
- Language Tour: TypeSpec Language Basics
- HTTP APIs: REST API Guide
- Advanced: Custom Decorators
Conclusion π
Weβve successfully transformed our weather application from implementation-first to contract-first development using TypeSpec. By defining our API contract in TypeSpec, weβve eliminated the manual coordination between frontend and backend teams while gaining better type safety, comprehensive documentation, and automated code generation.
What We Built:
- β TypeSpec API Definition - Single source of truth with clean, readable syntax
- β OpenAPI Specification - Auto-generated from TypeSpec for tooling compatibility
- β C# Server Code - Generated interfaces and models for .NET API
- β Interactive Documentation - Swagger UI integrated with Aspire dashboard
- β End-to-End Type Safety - Contract drives both frontend and backend implementation
Key Benefits:
- No More Drift - Frontend and backend canβt get out of sync
- Parallel Development - Teams work from the same contract simultaneously
- Better Quality - Validation and error handling defined upfront
- Future-Proof - Works with existing OpenAPI ecosystem and tools
Our weather API now demonstrates contract-first development that can scale across teams and projects. The TypeSpec definition becomes the authoritative source that drives both implementation and documentation.
Next Steps in Our Platform Journey
Now that we have a robust local development environment with Aspire and contract-first APIs with TypeSpec, itβs time to think about deploying our application to production. In our next post, weβll explore how to use Terraform to create the cloud infrastructure needed to host our weather application at scale.
Coming Up Next: Infrastructure as Code with Terraform: Deploying Our Platform to Azure βοΈ