The AI-Native IDP -- Parte 3
Software Templates con IA
El Problema
En el artículo 1 creamos un template de .NET API. Genera un proyecto con PostgreSQL, JWT auth y manifiestos de Kubernetes. Siempre igual. Para cada servicio.
Pero no todos los servicios necesitan PostgreSQL. No todos necesitan JWT. Algunos necesitan Redis. Otros necesitan Service Bus. Algunos necesitan todo. El template no lo sabe.
La solución típica es crear más templates. Uno para “API con PostgreSQL.” Otro para “API con Redis.” Otro para “Worker con Service Bus.” Otro para “API con PostgreSQL y Redis y Service Bus.” El número de templates crece con cada combinación. El equipo de plataforma acaba manteniendo templates en vez de construir la plataforma.
Y aunque tengas el template correcto, el proyecto generado es genérico. El README dice “Your service description here.” La configuración tiene valores de ejemplo. El developer todavía tiene que repasar cada archivo y personalizarlo para su caso concreto.
Hay una oportunidad más grande que nos estamos perdiendo: el momento en que un developer crea un servicio nuevo es el momento perfecto para prepararlo con desarrollo asistido por IA. Si el proyecto viene con un prompt GOTCHA preconfigurado — ya relleno con los Goals, Tools y Context correctos para este servicio concreto — el developer puede empezar a trabajar con IA directamente, sin perder 30 minutos escribiendo un prompt desde cero.
La Solución
Reemplazar el template estático por un scaffolder con IA. El developer describe lo que necesita en lenguaje natural:
“Una API .NET que gestiona facturas. Se conecta a PostgreSQL para almacenamiento y publica eventos a Azure Service Bus cuando se crea o paga una factura. Necesita JWT auth via QuantumID. Desplegada en Kubernetes.”
El scaffolder:
- Analiza la descripción con el modelo de IA
- Determina las dependencias, configuración e infraestructura correctas
- Genera los archivos del proyecto — no desde un esqueleto fijo, sino adaptado a la petición
- Crea un prompt GOTCHA pre-rellenado con el contexto específico del servicio
- Registra el nuevo servicio en el catálogo
Un template. Muchas variaciones. Y cada proyecto arranca con documentación lista para IA.
Execute
El Endpoint del Servicio de IA
Extendemos el servicio .NET de IA del artículo 2 con un nuevo endpoint:
app.MapPost("/api/scaffold", async (ScaffoldRequest request, IConfiguration config) =>
{
if (string.IsNullOrWhiteSpace(request.Description))
return Results.BadRequest(new { error = "Description is required." });
var endpoint = config["AI:Endpoint"];
var apiKey = config["AI:Key"];
var model = config["AI:ChatModel"] ?? "mistral-small-3.2-24b-instruct-2506";
var provider = config["AI:Provider"] ?? "openai";
ChatClient chatClient = provider.ToLowerInvariant() switch
{
"azure" => new AzureOpenAIClient(
new Uri(endpoint!), new ApiKeyCredential(apiKey!))
.GetChatClient(model),
_ => new OpenAIClient(
new ApiKeyCredential(apiKey!),
new OpenAIClientOptions { Endpoint = new Uri(endpoint!) })
.GetChatClient(model),
};
var systemPrompt = """
You are a project scaffolder for a .NET cloud platform.
Given a service description, produce a JSON object with:
- "name": kebab-case service name (e.g. "invoice-api")
- "description": one-line description (max 120 chars)
- "type": "api" | "worker" | "function"
- "dependencies": object with boolean flags:
{ "postgresql": bool, "redis": bool, "serviceBus": bool, "blobStorage": bool }
- "auth": "quantumid-jwt" | "api-key" | "none"
- "kubernetes": true | false
- "nugetPackages": array of NuGet package names needed
- "envVars": array of environment variable names the service will need
- "gotchaPrompt": a complete GOTCHA prompt with all 6 layers filled in
for this specific service. Use the GOTCHA format:
GOALS, ORCHESTRATION, TOOLS, CONTEXT, HEURISTICS, ARGS.
Be specific to this service — not generic.
Respond ONLY with valid JSON, no markdown.
""";
try
{
var completion = await chatClient.CompleteChatAsync(
[
new SystemChatMessage(systemPrompt),
new UserChatMessage(request.Description),
]);
var raw = completion.Value.Content[0].Text.Trim();
var json = raw.StartsWith("```") ? raw.Split('\n', 2)[1].TrimEnd('`').Trim() : raw;
var scaffold = JsonSerializer.Deserialize<ScaffoldResult>(json, SerializerOptions.Default);
if (scaffold is null)
return Results.UnprocessableEntity(new { error = "AI returned invalid scaffold spec." });
// Normalize gotchaPrompt: the AI may return it as an object or a string
var gotchaStr = scaffold.GotchaPrompt.ValueKind == JsonValueKind.String
? scaffold.GotchaPrompt.GetString() ?? ""
: scaffold.GotchaPrompt.ToString();
return Results.Ok(new
{
scaffold.Name, scaffold.Description, scaffold.Type,
scaffold.Dependencies, scaffold.Auth, scaffold.Kubernetes,
scaffold.NugetPackages, scaffold.EnvVars,
gotchaPrompt = gotchaStr,
});
}
catch (ClientResultException ex) when (ex.Status == 401)
{
return Results.Json(new { error = "AI provider authentication failed." }, statusCode: 503);
}
catch (Exception ex)
{
return Results.Json(new { error = $"AI provider error: {ex.Message}" }, statusCode: 502);
}
});
record ScaffoldRequest(string Description);
record ScaffoldResult(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("description")] string Description,
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("dependencies")] Dictionary<string, bool> Dependencies,
[property: JsonPropertyName("auth")] string Auth,
[property: JsonPropertyName("kubernetes")] bool Kubernetes,
[property: JsonPropertyName("nugetPackages")] List<string> NugetPackages,
[property: JsonPropertyName("envVars")] List<string> EnvVars,
[property: JsonPropertyName("gotchaPrompt")] JsonElement GotchaPrompt);
Usa el mismo patrón de provider de IA del artículo 2 — mismo IConfiguration, mismo manejo de errores. El campo gotchaPrompt usa JsonElement porque algunos modelos lo devuelven como string y otros como un objeto estructurado. Lo normalizamos a string antes de enviarlo al scaffolder.
Lo Que Devuelve la IA
Para la descripción de la API de facturas, el servicio de IA devuelve algo como:
{
"name": "invoice-api",
"description": "REST API for invoice management with event-driven notifications via Service Bus",
"type": "api",
"dependencies": {
"postgresql": true,
"redis": false,
"serviceBus": true,
"blobStorage": false
},
"auth": "quantumid-jwt",
"kubernetes": true,
"nugetPackages": [
"Npgsql.EntityFrameworkCore.PostgreSQL",
"Azure.Messaging.ServiceBus",
"Microsoft.AspNetCore.Authentication.JwtBearer",
"Serilog.AspNetCore",
"FluentValidation.AspNetCore"
],
"envVars": [
"DB_CONNECTION",
"SERVICEBUS_CONNECTION",
"QUANTUMID_AUTHORITY",
"QUANTUMID_AUDIENCE"
],
"gotchaPrompt": "=== GOALS ===\nBuild a .NET 10 REST API for invoice management...[full prompt]"
}
El prompt GOTCHA es la parte mas valiosa. No es un template genérico — es específico para este servicio, con las herramientas correctas, las variables de entorno correctas y heurísticas que encajan con las dependencias.
La Scaffolder Action de Backstage
El scaffolder de Backstage soporta acciones personalizadas. Creamos una que llama al servicio de IA y genera el proyecto:
// plugins/ai-scaffolder/src/actions/aiScaffold.ts
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import * as fs from 'fs-extra';
import * as path from 'path';
export function createAiScaffoldAction(aiServiceUrl: string) {
return createTemplateAction({
id: 'forge:ai-scaffold',
schema: {
input: {
description: z => z.string().describe('Service Description'),
owner: z => z.string().describe('Owner'),
},
output: {
name: z => z.string().optional(),
gotchaPrompt: z => z.string().optional(),
},
},
async handler(ctx) {
const { description, owner } = ctx.input;
ctx.logger.info(`Generating scaffold for: ${description}`);
const res = await fetch(`${aiServiceUrl}/api/scaffold`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description }),
});
if (!res.ok) {
throw new Error(`AI scaffold service returned ${res.status}`);
}
const scaffold = await res.json();
const projectDir = ctx.workspacePath;
// Generate Program.cs
await fs.writeFile(
path.join(projectDir, 'Program.cs'),
generateProgramCs(scaffold),
);
// Generate .csproj with the right NuGet packages
await fs.writeFile(
path.join(projectDir, `${scaffold.name}.csproj`),
generateCsproj(scaffold),
);
// Generate appsettings.json with env var placeholders
await fs.writeFile(
path.join(projectDir, 'appsettings.json'),
generateAppSettings(scaffold),
);
// Generate Dockerfile
await fs.writeFile(
path.join(projectDir, 'Dockerfile'),
generateDockerfile(scaffold),
);
// Generate catalog-info.yaml
await fs.writeFile(
path.join(projectDir, 'catalog-info.yaml'),
generateCatalogInfo(scaffold, owner),
);
// Generate the GOTCHA prompt
await fs.writeFile(
path.join(projectDir, 'GOTCHA.md'),
formatGotchaPrompt(scaffold),
);
// Generate K8s manifests if needed
if (scaffold.kubernetes) {
await fs.ensureDir(path.join(projectDir, 'k8s'));
await fs.writeFile(
path.join(projectDir, 'k8s', 'deployment.yaml'),
generateK8sDeployment(scaffold),
);
await fs.writeFile(
path.join(projectDir, 'k8s', 'service.yaml'),
generateK8sService(scaffold),
);
}
ctx.output('name', scaffold.name);
ctx.output('gotchaPrompt', scaffold.gotchaPrompt);
ctx.logger.info(`Scaffold complete: ${scaffold.name}`);
},
});
}
Backstage 1.37 usa Zod para validación de schemas en las acciones del scaffolder. El parámetro z lo inyecta el framework — no hace falta importarlo. Si usas una versión anterior de Backstage, el formato del schema usa objetos JSON Schema en su lugar.
Los Generadores de Archivos
Cada generador produce un archivo real y funcional — no un placeholder:
// plugins/ai-scaffolder/src/generators/programCs.ts
interface ScaffoldResult {
name: string;
dependencies: Record<string, boolean>;
auth: string;
envVars: string[];
}
export function generateProgramCs(scaffold: ScaffoldResult): string {
const lines: string[] = [];
// Usings based on actual dependencies
if (scaffold.dependencies.postgresql) {
lines.push('using Microsoft.EntityFrameworkCore;');
}
if (scaffold.dependencies.serviceBus) {
lines.push('using Azure.Messaging.ServiceBus;');
}
lines.push('using Serilog;');
lines.push('');
lines.push('var builder = WebApplication.CreateBuilder(args);');
lines.push('');
lines.push('builder.Host.UseSerilog((ctx, config) =>');
lines.push(' config.ReadFrom.Configuration(ctx.Configuration));');
lines.push('');
// Database
if (scaffold.dependencies.postgresql) {
lines.push('builder.Services.AddDbContext<AppDbContext>(options =>');
lines.push(' options.UseNpgsql(');
lines.push(' builder.Configuration.GetConnectionString("Default")));');
lines.push('');
}
// Service Bus
if (scaffold.dependencies.serviceBus) {
lines.push('builder.Services.AddSingleton(_ =>');
lines.push(' new ServiceBusClient(');
lines.push(' Environment.GetEnvironmentVariable("SERVICEBUS_CONNECTION")));');
lines.push('');
}
// Auth
if (scaffold.auth === 'quantumid-jwt') {
lines.push('builder.Services');
lines.push(' .AddAuthentication("Bearer")');
lines.push(' .AddJwtBearer(options =>');
lines.push(' {');
lines.push(' options.Authority = Environment.GetEnvironmentVariable("QUANTUMID_AUTHORITY");');
lines.push(' options.Audience = Environment.GetEnvironmentVariable("QUANTUMID_AUDIENCE");');
lines.push(' });');
lines.push('builder.Services.AddAuthorization();');
lines.push('');
}
lines.push('var app = builder.Build();');
lines.push('');
if (scaffold.auth === 'quantumid-jwt') {
lines.push('app.UseAuthentication();');
lines.push('app.UseAuthorization();');
lines.push('');
}
lines.push('app.MapGet("/healthz", () => Results.Ok("healthy"));');
lines.push('');
lines.push('app.Run();');
return lines.join('\n');
}
El Archivo GOTCHA.md
Esto es lo que hace a este scaffolder diferente de cualquier otro template. Cada proyecto generado incluye un archivo GOTCHA.md:
# GOTCHA Prompt — invoice-api
Use this prompt with your AI tool to develop this service.
Generated by Forge based on your service description.
## GOALS
Build a .NET 10 REST API for invoice management.
CRUD operations for invoices (create, read, update, delete).
Publish InvoiceCreated and InvoicePaid events to Azure Service Bus.
JWT authentication via QuantumID. RFC 7807 error responses.
Handle 200 concurrent requests.
## ORCHESTRATION
1. Entity models (Invoice, InvoiceItem)
2. EF Core DbContext + migrations
3. Repository layer
4. Service Bus publisher
5. Endpoint handlers (Minimal API)
6. Validators (FluentValidation)
7. Auth middleware
8. Health check endpoint
## TOOLS
- .NET 10, ASP.NET Minimal API
- EF Core 10 + Npgsql
- Azure.Messaging.ServiceBus
- FluentValidation
- Serilog
- QuantumID (OIDC JWT)
## CONTEXT
- Part of the victorz-cloud platform
- Deploys to Kubernetes (Scaleway Kapsule)
- PostgreSQL database (connection via DB_CONNECTION env var)
- Service Bus (connection via SERVICEBUS_CONNECTION env var)
- Auth: QuantumID JWT (QUANTUMID_AUTHORITY, QUANTUMID_AUDIENCE)
- All logs structured via Serilog
## HEURISTICS
DO:
- Async everywhere — no .Result or .Wait()
- Repository pattern — controllers don't touch DbContext
- Return DTOs, not entities
- Publish events after successful DB transaction only
- Soft delete via DeletedAt nullable column
DON'T:
- No secrets in code — all from environment variables
- No catching base Exception — catch specific types
- No synchronous Service Bus sends in request pipeline
- Don't expose internal IDs in events — use correlation IDs
## ARGS
DB_CONNECTION: env var
SERVICEBUS_CONNECTION: env var
QUANTUMID_AUTHORITY: https://auth.quantumapi.eu
QUANTUMID_AUDIENCE: invoice-api
Port: 8080
K8s replicas: 2
Health check: /healthz
El developer abre este archivo, lo copia en Claude o cualquier herramienta de IA y empieza a construir. La IA ya conoce el stack, el entorno, las reglas y los valores concretos. Sin perder 30 minutos escribiendo un prompt — está listo desde el minuto cero.
El Template YAML
El template de Backstage que usa la acción personalizada:
# templates/ai-service/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: ai-service
title: AI-Powered Service (describe what you need)
description: Describe your service in plain English. AI generates the project with the right dependencies and a GOTCHA prompt.
tags:
- ai
- recommended
spec:
owner: team-platform
type: service
parameters:
- title: What do you need?
required:
- description
- owner
properties:
description:
title: Describe your service
type: string
ui:widget: textarea
ui:options:
rows: 4
ui:placeholder: "A .NET API that manages invoices. It connects to PostgreSQL and publishes events to Service Bus when an invoice is created or paid. JWT auth via QuantumID. Deployed to Kubernetes."
owner:
title: Owner
type: string
ui:field: OwnerPicker
steps:
- id: ai-scaffold
name: Generate Project
action: forge:ai-scaffold
input:
description: ${{ parameters.description }}
owner: ${{ parameters.owner }}
- id: publish
name: Publish to GitHub
action: publish:github
input:
allowedHosts: ["github.com"]
repoUrl: github.com?owner=victorZKov&repo=${{ steps['ai-scaffold'].output.name }}
description: ${{ parameters.description }}
defaultBranch: main
- id: register
name: Register in Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
output:
links:
- title: Repository
url: ${{ steps.publish.output.remoteUrl }}
- title: Open in Catalog
icon: catalog
entityRef: ${{ steps.register.output.entityRef }}
Tu prompt GOTCHA está listo en GOTCHA.md — ábrelo y pégalo en tu herramienta de IA para empezar a construir.
La UI muestra un textarea. El developer escribe lo que necesita. Hace clic en “Create.” Recibe un repo con todo configurado y un prompt GOTCHA listo.
Registrar la Acción Personalizada
Las acciones personalizadas del scaffolder en el nuevo sistema de backend de Backstage se registran como módulos en el plugin scaffolder:
// packages/backend/src/modules/aiScaffoldModule.ts
import { createBackendModule } from '@backstage/backend-plugin-api';
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node';
import { coreServices } from '@backstage/backend-plugin-api';
import { createAiScaffoldAction } from '@internal/plugin-ai-scaffolder';
export const aiScaffoldModule = createBackendModule({
pluginId: 'scaffolder',
moduleId: 'ai-scaffold',
register(reg) {
reg.registerInit({
deps: {
scaffolder: scaffolderActionsExtensionPoint,
config: coreServices.rootConfig,
},
async init({ scaffolder, config }) {
const aiServiceUrl = config.getString('forge.aiServiceUrl');
scaffolder.addActions(createAiScaffoldAction(aiServiceUrl));
},
});
},
});
Luego en packages/backend/src/index.ts:
import { aiScaffoldModule } from './modules/aiScaffoldModule';
backend.add(aiScaffoldModule);
Y la configuración en app-config.yaml:
forge:
aiServiceUrl: http://localhost:5100
Si usas Node.js 20 o posterior, arranca Backstage con NODE_OPTIONS=--no-node-snapshot — el scaffolder lo necesita.
Checklist
- El endpoint de scaffold (
/api/scaffold) devuelve una especificación de proyecto válida - La acción personalizada del scaffolder genera todos los archivos del proyecto
- El
Program.csgenerado compila y funciona con las dependencias especificadas - GOTCHA.md es específico para el servicio descrito (no genérico)
- El template YAML está registrado en Backstage y visible en la UI
- El proyecto generado incluye
catalog-info.yamlcon los metadatos correctos - Los manifiestos de Kubernetes se generan cuando
kubernetes: true
Antes del Siguiente Artículo
Ya puedes crear un servicio describiendo lo que necesitas. El proyecto viene con las dependencias correctas y un prompt GOTCHA listo para desarrollo asistido por IA.
Pero, y el código que escribes después de crear el proyecto? Cada PR necesita revisión. La mayoría del code review pasa después del hecho — el reviewer ve el diff, pero no sabe si el código sigue los patrones arquitectónicos del equipo o si encaja con el rol del servicio en el sistema.
Y si el reviewer tuviera acceso al catálogo de servicios? Y si supiera que este PR es para la invoice-api, que debería usar el patrón repository, y que nunca debería llamar a Service Bus de forma síncrona?
Eso es el artículo 4: The AI Code Review Plugin.
Si esta serie te resulta útil, puedes invitarme a un café.
Este es el artículo 3 de la serie AI-Native IDP. Siguiente: The AI Code Review Plugin — revisiones de PR con contexto arquitectónico del catálogo.
Loading comments...