Por Qué Smithy Sobre OpenAPI
OpenAPI describe APIs HTTP. Eso es tanto su fortaleza como su limitación. Cuando escribirs una spec OpenAPI, se está describiendo paths, métodos, request bodies, y response schemas. El transporte HTTP está metido en el modelo.
Smithy describe APIs. Punto. El transporte es una concern separada aplicada via protocol traits. Esto significa que el mismo modelo Smithy puede generar un SDK REST/JSON, un cliente gRPC, un cliente event-driven, documentación, y server stubs.
Acá va cómo se ve un modelo Smithy para un servicio simple:
$version: "2"
namespace com.example.inventory
use aws.protocols#restJson1
@restJson1
@title("Inventory Service")
@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "maxResults")
service InventoryService {
version: "2026-01-15"
operations: [ListProducts, GetProduct, CreateProduct, UpdateProduct, DeleteProduct]
resources: [Product]
}
resource Product {
identifiers: { productId: ProductId }
read: GetProduct
create: CreateProduct
update: UpdateProduct
delete: DeleteProduct
list: ListProducts
}
@readonly
@http(method: "GET", uri: "/products/{productId}")
operation GetProduct {
input := {
@required @httpLabel
productId: ProductId
}
output := {
@required
product: ProductShape
}
errors: [ProductNotFound, ValidationError]
}
@http(method: "POST", uri: "/products")
operation CreateProduct {
input := {
@required name: String
@required sku: String
price: BigDecimal
@length(min: 0, max: 10)
tags: TagList
metadata: MetadataMap
}
output := { @required product: ProductShape }
errors: [ConflictError, ValidationError]
}
Observar varias cosas:
-
Los recurse es son first-class.
Productse declara como un recurso con operaciones CRUD mapeadas. Esto no es solo documentación, el generador de código usa las relaciones de recurse es para generar métodos helper comoproduct.refresh()oproduct.delete(). -
Las constraints están en el modelo.
@range(min: 1, max: 100)enmaxResultssignifica que el SDK generado valida esto client-side antes de hacer un network call. -
La paginación es declarativa. El trait
@paginateden el servicio y la operación le dice al generador que produzca helpers de auto-paginación. -
Los errores son tipados. Cada estructura de error mapea a un HTTP status code específico. El SDK generado produce clases de error tipadas que el caller puede catchear específicamente.
El Sistema de Traits
La característica más destacada de Smithy son los traits. Un trait es metadata adjunta a cualquier shape que modifica comportamiento o agrega información. AWS define cientos, pero es posible crear los tuyos:
@trait(selector: "operation")
structure rateLimit {
@required requestsPerSecond: Integer
burstSize: Integer
}
@rateLimit(requestsPerSecond: 100, burstSize: 200)
operation CreateProduct { ... }
Nuestro trait custom @rateLimit le dice al generador de SDK que agregue rate limiting client-side. Estos no son solo anotaciones, manejan generación de código.
Validadores Custom
Smithy tiene un pipeline de validación que corre sobre tu modelo antes de la generación de código. Escribimos validadores custom para enforcer nuestros estándares de diseño de API:
public final class PaginationValidator extends AbstractValidator {
@Override
public List<ValidationEvent> validate(Model model) {
List<ValidationEvent> events = new ArrayList<>();
for (OperationShape op : model.getOperationShapes()) {
if (op.getId().getName().startsWith("List")
&& !op.hasTrait(PaginatedTrait.class)) {
events.add(danger(op,
"List operations must use the @paginated trait. "
+ "Operation: " + op.getId()));
}
}
for (StructureShape struct : model.getStructureShapes()) {
boolean hasCreatedAt = struct.getMember("createdAt").isPresent();
boolean hasUpdatedAt = struct.getMember("updatedAt").isPresent();
if (hasCreatedAt && !hasUpdatedAt) {
events.add(danger(struct,
"Structures with 'createdAt' must also have 'updatedAt'."));
}
}
return events;
}
}
Este validador corre como parte de smithy build y falla el build si alguna operación de listado no está paginada o si los campos de auditoría de timestamps están incompletos. Atrapó tres configuraciones de paginación faltantes en nuestros modelos en la primera semana.
Armando un Generador de SDK Custom
El poder real viene cuando armars tu propio generador de código. El framework codegen de Smithy te da un modelo, un set de integraciones, y usted producís archivos. El TypeScript generado se ve así:
// Generated by smithy-typescript-codegen - DO NOT EDIT
export interface Product {
productId: string;
name: string;
sku: string;
price?: number;
tags?: string[];
metadata?: Record<string, string>;
createdAt: Date;
updatedAt?: Date;
}
export class ProductNotFoundError extends ServiceError {
readonly name = 'ProductNotFound';
readonly statusCode = 404;
readonly productId?: string;
}
export class InventoryServiceClient {
private readonly http: HttpClient;
constructor(config: ClientConfig) {
this.http = new HttpClient(config);
}
async getProduct(input: GetProductInput, options?: RequestOptions): Promise<GetProductOutput> {
if (!input.productId) throw new ValidationError('productId is required');
return this.http.request({
method: 'GET',
path: `/products/${encodeURIComponent(input.productId)}`,
...options,
});
}
async *paginateListProducts(
input: ListProductsInput = {},
options?: RequestOptions
): AsyncGenerator<Product[], void, undefined> {
let nextToken = input.nextToken;
do {
const response = await this.listProducts({ ...input, nextToken }, options);
yield response.products;
nextToken = response.nextToken;
} while (nextToken);
}
}
El paginador se genera automáticamente porque ListProducts tiene el trait @paginated. Las clases de error se generan porque la operación declara errors: [ProductNotFound, ValidationError]. La lógica de validación se genera desde los traits @required y @range.
El Pipeline de Build
Nuestro pipeline completo:
{
"version": "1.0",
"sources": ["model"],
"plugins": {
"openapi": {
"service": "com.example.inventory#InventoryService",
"protocol": "aws.protocols#restJson1"
},
"typescript-sdk": {
"service": "com.example.inventory#InventoryService",
"packageName": "@example/inventory-sdk"
},
"python-sdk": {
"service": "com.example.inventory#InventoryService",
"packageName": "example-inventory"
}
}
}
Un solo comando smithy build genera cuatro SDKs, una spec OpenAPI, y valida el modelo contra nuestros estándares. Los SDKs generados se publican a registries internos via CI. Cuando un ingeniero cambia el modelo Smithy y mergea, los cuatro SDKs se regeneran, se testean contra el servicio corriendo, y se publican, automáticamente.
¿Valió la Pena?
El setup inicial tomó unas tres semanas, una semana aprendiendo Smithy, una semana armando los generadores de código, una semana cableando CI/CD. Desde entonces:
- Zero drift entre SDKs y la API real
- Operaciones nuevas van de cambio en el modelo a SDK publicado en 12 minutos
- Los validadores custom previnieron 14 inconsistencias de diseño de API antes de que llegaran a producción
- El onboarding de ingenieros al SDK es trivial, el código generado es consistente entre todos los servicios
La curva de aprendizaje de Smithy es más empinada que la de OpenAPI, y el toolchain Java-centric es un dolor si tu equipo no está cómodo con la JVM. Pero para generación de SDKs multi-lenguaje con type safety y validación baked-in, nada se le acerca. La inversión se paga sola en un trimestre.