{"schemaVersion":1,"id":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation","title":"Smithy: AWS's API Modeling Language and Building Your Own SDK Generator","description":"An examination of Smithy, AWS's protocol-agnostic API modeling language, its code generation pipeline, custom validators, trait system, and how to build a custom SDK generator from a Smithy model.","language":"en","datePublished":"2025-11-07","dateModified":null,"contentFormat":"text/markdown","content":"API Design\n\n# Smithy: AWS's API Modeling Language and Building Your Own SDK Generator\n\n2025-11-078 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/smithy-api-modeling-aws-sdk-generation>)\n\n## Why Smithy Over OpenAPI\n\nOpenAPI describes HTTP APIs. That's both its strength and its limitation. When you write an OpenAPI spec, you're describing paths, methods, request bodies, and response schemas. The HTTP transport is baked into the model.\n\n[Smithy](<https://smithy.io/>) describes APIs. Period. The transport is a separate concern applied via protocol traits. This means the same Smithy model can generate:\n\n- A REST/JSON SDK\n- A gRPC client\n- An event-driven client (for WebSocket or MQTT APIs)\n- API documentation\n- Server stubs\n\nHere's what a Smithy model looks like for a simple service:\n\n```smithy\n$version: \"2\"\n\nnamespace com.example.inventory\n\nuse aws.protocols#restJson1\n\n@restJson1\n@title(\"Inventory Service\")\n@paginated(inputToken: \"nextToken\", outputToken: \"nextToken\", pageSize: \"maxResults\")\nservice InventoryService {\n version: \"2026-01-15\"\n operations: [\n ListProducts\n GetProduct\n CreateProduct\n UpdateProduct\n DeleteProduct\n ]\n resources: [Product]\n}\n\nresource Product {\n identifiers: { productId: ProductId }\n read: GetProduct\n create: CreateProduct\n update: UpdateProduct\n delete: DeleteProduct\n list: ListProducts\n}\n\nstring ProductId\n\n@readonly\n@http(method: \"GET\", uri: \"/products/{productId}\")\noperation GetProduct {\n input := {\n @required\n @httpLabel\n productId: ProductId\n }\n output := {\n @required\n product: ProductShape\n }\n errors: [ProductNotFound, ValidationError]\n}\n\n@http(method: \"POST\", uri: \"/products\")\noperation CreateProduct {\n input := {\n @required\n name: String\n @required\n sku: String\n price: BigDecimal\n @length(min: 0, max: 10)\n tags: TagList\n metadata: MetadataMap\n }\n output := {\n @required\n product: ProductShape\n }\n errors: [ConflictError, ValidationError]\n}\n\n@paginated\n@readonly\n@http(method: \"GET\", uri: \"/products\")\noperation ListProducts {\n input := {\n @httpQuery(\"maxResults\")\n @range(min: 1, max: 100)\n maxResults: Integer\n @httpQuery(\"nextToken\")\n nextToken: String\n @httpQuery(\"category\")\n category: String\n }\n output := {\n @required\n products: ProductList\n nextToken: String\n }\n}\n\nstructure ProductShape {\n @required\n productId: ProductId\n @required\n name: String\n @required\n sku: String\n price: BigDecimal\n tags: TagList\n metadata: MetadataMap\n @required\n createdAt: Timestamp\n updatedAt: Timestamp\n}\n\nlist ProductList {\n member: ProductShape\n}\n\nlist TagList {\n member: String\n}\n\nmap MetadataMap {\n key: String\n value: String\n}\n\n@error(\"client\")\n@httpError(404)\nstructure ProductNotFound {\n @required\n message: String\n productId: ProductId\n}\n\n@error(\"client\")\n@httpError(409)\nstructure ConflictError {\n @required\n message: String\n}\n\n@error(\"client\")\n@httpError(400)\nstructure ValidationError {\n @required\n message: String\n fieldErrors: FieldErrorList\n}\n\nlist FieldErrorList {\n member: FieldError\n}\n\nstructure FieldError {\n @required\n field: String\n @required\n message: String\n}\n```\n\nNotice several things:\n\n1. **Resources are first-class.** ` Product ` is declared as a resource with CRUD operations mapped to it. This isn't just documentation, the code generator uses resource relationships to generate helper methods like ` product.refresh() ` or ` product.delete() `.\n2. **Constraints are in the model.** ` @range(min: 1, max: 100) ` on ` maxResults ` means the generated SDK validates this client-side before making a network call. ` @length(min: 0, max: 10) ` on tags does the same.\n3. **Pagination is declarative.** The ` @paginated ` trait on the service and operation tells the generator to produce auto-pagination helpers. In the TypeScript SDK, this becomes a ` paginateListProducts() ` function that returns an async iterator.\n4. **Errors are typed.** Each error structure maps to a specific HTTP status code. The generated SDK produces typed error classes that the caller can catch specifically.\n\n## The Trait System\n\nSmithy's killer feature is traits. A trait is metadata attached to any shape (structure, operation, service, member) that modifies behavior or adds information. [AWS](<https://aws.com/>) defines hundreds of traits, but you can create your own:\n\n```smithy\n@trait(selector: \"operation\")\nstructure rateLimit {\n @required\n requestsPerSecond: Integer\n burstSize: Integer\n}\n\n@trait(selector: \"structure > member\")\nstructure sensitive {}\n\n@rateLimit(requestsPerSecond: 100, burstSize: 200)\noperation CreateProduct { ... }\n\nstructure UserProfile {\n name: String\n @sensitive\n email: String\n @sensitive\n ssn: String\n}\n```\n\nOur custom ` @rateLimit ` trait tells the SDK generator to add client-side rate limiting. The ` @sensitive ` trait (this one is built-in, actually) tells the generator to redact the value in logs and error messages. These aren't just annotations, they drive code generation.\n\n## Custom Validators\n\nSmithy has a validation pipeline that runs on your model before code generation. We wrote custom validators to enforce our API design standards:\n\n```java\npublic final class PaginationValidator extends AbstractValidator {\n @Override\n public List<ValidationEvent> validate(Model model) {\n List<ValidationEvent> events = new ArrayList<>();\n\n for (OperationShape op : model.getOperationShapes()) {\n // Every list operation must be paginated\n if (op.getId().getName().startsWith(\"List\")\n && !op.hasTrait(PaginatedTrait.class)) {\n events.add(danger(op,\n \"List operations must use the @paginated trait. \"\n + \"Operation: \" + op.getId()));\n }\n\n // All operations must have at least one error\n if (op.getErrors().isEmpty()) {\n events.add(warning(op,\n \"Operations should define at least one error type. \"\n + \"Operation: \" + op.getId()));\n }\n }\n\n // All structures with a 'createdAt' must also have 'updatedAt'\n for (StructureShape struct : model.getStructureShapes()) {\n boolean hasCreatedAt = struct.getMember(\"createdAt\").isPresent();\n boolean hasUpdatedAt = struct.getMember(\"updatedAt\").isPresent();\n\n if (hasCreatedAt && !hasUpdatedAt) {\n events.add(danger(struct,\n \"Structures with 'createdAt' must also have 'updatedAt'. \"\n + \"Structure: \" + struct.getId()));\n }\n }\n\n return events;\n }\n}\n```\n\nThis validator runs as part of ` smithy build ` and fails the build if any list operation isn't paginated or if timestamp audit fields are incomplete. It caught three missing pagination configurations in our models within the first week.\n\n## Building a Custom SDK Generator\n\nThe real power comes when you build your own code generator. Smithy's codegen framework gives you a model, a set of integrations, and you produce files. Here's a simplified version of how our TypeScript generator works:\n\n```java\npublic final class TypeScriptCodegenPlugin implements SmithyBuildPlugin {\n\n @Override\n public String getName() {\n return \"typescript-sdk\";\n }\n\n @Override\n public void execute(PluginContext context) {\n Model model = context.getModel();\n ServiceShape service = model.expectShape(\n context.getSettings().getStringMember(\"service\")\n .map(ShapeId::from)\n .orElseThrow(),\n ServiceShape.class\n );\n\n TypeScriptWriter writer = new TypeScriptWriter();\n\n // Generate client class\n generateClient(writer, model, service);\n\n // Generate types for all structures\n for (StructureShape struct : model.getStructureShapes()) {\n if (isInService(model, service, struct)) {\n generateInterface(writer, model, struct);\n }\n }\n\n // Generate operation functions\n for (OperationShape op : getServiceOperations(model, service)) {\n generateOperation(writer, model, op);\n\n // Generate paginator if paginated\n if (op.hasTrait(PaginatedTrait.class)) {\n generatePaginator(writer, model, op);\n }\n }\n\n // Generate error classes\n for (StructureShape error : getServiceErrors(model, service)) {\n generateErrorClass(writer, model, error);\n }\n\n // Write all files\n writer.flushTo(context.getFileManifest());\n }\n}\n```\n\nThe generated TypeScript client looks like this:\n\n```typescript\n// Generated by smithy-typescript-codegen - DO NOT EDIT\nimport { HttpClient, RequestOptions } from './runtime';\n\nexport interface GetProductInput {\n productId: string;\n}\n\nexport interface GetProductOutput {\n product: Product;\n}\n\nexport interface Product {\n productId: string;\n name: string;\n sku: string;\n price?: number;\n tags?: string[];\n metadata?: Record<string, string>;\n createdAt: Date;\n updatedAt?: Date;\n}\n\nexport class ProductNotFoundError extends ServiceError {\n readonly name = 'ProductNotFound';\n readonly statusCode = 404;\n readonly productId?: string;\n\n constructor(message: string, productId?: string) {\n super(message);\n this.productId = productId;\n }\n}\n\nexport class InventoryServiceClient {\n private readonly http: HttpClient;\n\n constructor(config: ClientConfig) {\n this.http = new HttpClient(config);\n }\n\n async getProduct(\n input: GetProductInput,\n options?: RequestOptions\n ): Promise<GetProductOutput> {\n // Client-side validation (from @required trait)\n if (!input.productId) {\n throw new ValidationError('productId is required');\n }\n\n return this.http.request({\n method: 'GET',\n path: `/products/${encodeURIComponent(input.productId)}`,\n ...options,\n });\n }\n\n async *paginateListProducts(\n input: ListProductsInput = {},\n options?: RequestOptions\n ): AsyncGenerator<Product[], void, undefined> {\n let nextToken = input.nextToken;\n do {\n const response = await this.listProducts(\n { ...input, nextToken },\n options\n );\n yield response.products;\n nextToken = response.nextToken;\n } while (nextToken);\n }\n}\n```\n\nThe paginator is automatically generated because ` ListProducts ` has the ` @paginated ` trait. The error classes are generated because the operation declares ` errors: [ProductNotFound, ValidationError] `. The validation logic is generated from the ` @required ` and ` @range ` traits.\n\n## The Build Pipeline\n\nOur complete pipeline:\n\n```\nsmithy-model/\n├── model/\n│ ├── inventory.smithy\n│ ├── orders.smithy\n│ └── common.smithy\n├── validators/\n│ └── ApiStandardsValidator.java\n├── smithy-build.json\n└── build/\n ├── typescript/\n ├── python/\n ├── go/\n └── rust/\n```\n\nThe ` smithy-build.json ` orchestrates everything:\n\n```json\n{\n \"version\": \"1.0\",\n \"sources\": [\"model\"],\n \"maven\": {\n \"dependencies\": [\n \"software.amazon.smithy:smithy-model:1.45.0\",\n \"software.amazon.smithy:smithy-openapi:1.45.0\",\n \"com.example:api-validators:1.0.0\"\n ]\n },\n \"plugins\": {\n \"openapi\": {\n \"service\": \"com.example.inventory#InventoryService\",\n \"protocol\": \"aws.protocols#restJson1\"\n },\n \"typescript-sdk\": {\n \"service\": \"com.example.inventory#InventoryService\",\n \"packageName\": \"@example/inventory-sdk\",\n \"packageVersion\": \"2.14.0\"\n },\n \"python-sdk\": {\n \"service\": \"com.example.inventory#InventoryService\",\n \"packageName\": \"example-inventory\"\n }\n }\n}\n```\n\nOne ` smithy build ` command generates four SDKs, an OpenAPI spec, and validates the model against our standards. The generated SDKs are published to internal registries via CI. When an engineer changes the Smithy model and merges, all four SDKs are regenerated, tested against the running service, and published, automatically.\n\n## Was It Worth It?\n\nThe initial setup took about three weeks, one week learning Smithy, one week building the code generators, one week wiring up CI/CD. Since then:\n\n- Zero drift between SDKs and the actual API\n- New operations go from model change to published SDK in 12 minutes\n- The custom validators have prevented 14 API design inconsistencies before they reached production\n- Engineer onboarding to the SDK is trivial, the generated code is consistent across all services\n\nSmithy's learning curve is steeper than OpenAPI's, and the Java-centric toolchain is a pain if your team isn't JVM-comfortable. But for multi-language SDK generation with type safety and validation baked in, nothing else comes close. The investment pays for itself within a quarter.\n\n## Frequently asked questions\n\n### How does Smithy compare to OpenAPI?\n\nSmithy is protocol-agnostic (it models APIs, not HTTP endpoints), supports resource-based modeling, has a built-in trait system for extensibility, and generates cleaner SDKs. OpenAPI is more widely adopted, has better tooling for REST documentation, and is the standard for HTTP-specific APIs. Smithy can generate OpenAPI specs from its models.\n\n### Can I use Smithy for non-AWS projects?\n\nAbsolutely. Smithy is an open-source project under Apache 2.0. While AWS uses it internally for all their services, the language and tooling are completely general-purpose. You can define any API, use any protocol binding, and generate SDKs for any language.\n\n### What languages can Smithy generate SDKs for?\n\nAWS officially maintains generators for TypeScript, Python, Java, Go, Rust, C++, Swift, Kotlin, Ruby, and PHP. The community has generators for C# and Dart. You can also build custom generators using the smithy-codegen framework.\n\n## Tips\n\n1. **LuchoTip:** Smithy's ` @paginated ` trait is far more powerful than it looks. Define it once on your list operations and the SDK generator can automatically produce both sync iterators and async pagination helpers. Don't roll your own pagination pattern.\n2. **LuchoTip:** Use Smithy's ` @examples ` trait on every operation. They serve as documentation, but more importantly, the code generator can turn them into integration test scaffolding that validates your SDK against real API responses.\n3. **LuchoTip:** When writing custom validators, always implement ` AbstractValidator<Shape> ` rather than checking shapes manually in a plugin. Validators get composed correctly in the validation pipeline and their diagnostics show up in IDEs with proper source locations.\n\n- smithy\n- api-design\n- aws\n- sdk-generation\n- code-generation\n- openapi\n- protocol-buffers\n- idl\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Smithy: AWS's API Modeling Language and Building Your Own SDK Generator","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#title"},{"level":2,"text":"Why Smithy Over OpenAPI","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#why-smithy-over-openapi"},{"level":2,"text":"The Trait System","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#the-trait-system"},{"level":2,"text":"Custom Validators","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#custom-validators"},{"level":2,"text":"Building a Custom SDK Generator","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#building-a-custom-sdk-generator"},{"level":2,"text":"The Build Pipeline","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#the-build-pipeline"},{"level":2,"text":"Was It Worth It?","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#was-it-worth-it"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#questions"},{"level":3,"text":"How does Smithy compare to OpenAPI?","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#question-1"},{"level":3,"text":"Can I use Smithy for non-AWS projects?","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#question-2"},{"level":3,"text":"What languages can Smithy generate SDKs for?","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#article-tips"}],"sections":[{"heading":"Smithy: AWS's API Modeling Language and Building Your Own SDK Generator","level":1,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation","content":"API Design"},{"heading":"Smithy: AWS's API Modeling Language and Building Your Own SDK Generator","level":1,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#title","content":"2025-11-078 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/smithy-api-modeling-aws-sdk-generation>)"},{"heading":"Why Smithy Over OpenAPI","level":2,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#why-smithy-over-openapi","content":"OpenAPI describes HTTP APIs. That's both its strength and its limitation. When you write an OpenAPI spec, you're describing paths, methods, request bodies, and response schemas. The HTTP transport is baked into the model.\n\n[Smithy](<https://smithy.io/>) describes APIs. Period. The transport is a separate concern applied via protocol traits. This means the same Smithy model can generate:\n\n- A REST/JSON SDK\n- A gRPC client\n- An event-driven client (for WebSocket or MQTT APIs)\n- API documentation\n- Server stubs\n\nHere's what a Smithy model looks like for a simple service:\n\n```smithy\n$version: \"2\"\n\nnamespace com.example.inventory\n\nuse aws.protocols#restJson1\n\n@restJson1\n@title(\"Inventory Service\")\n@paginated(inputToken: \"nextToken\", outputToken: \"nextToken\", pageSize: \"maxResults\")\nservice InventoryService {\n version: \"2026-01-15\"\n operations: [\n ListProducts\n GetProduct\n CreateProduct\n UpdateProduct\n DeleteProduct\n ]\n resources: [Product]\n}\n\nresource Product {\n identifiers: { productId: ProductId }\n read: GetProduct\n create: CreateProduct\n update: UpdateProduct\n delete: DeleteProduct\n list: ListProducts\n}\n\nstring ProductId\n\n@readonly\n@http(method: \"GET\", uri: \"/products/{productId}\")\noperation GetProduct {\n input := {\n @required\n @httpLabel\n productId: ProductId\n }\n output := {\n @required\n product: ProductShape\n }\n errors: [ProductNotFound, ValidationError]\n}\n\n@http(method: \"POST\", uri: \"/products\")\noperation CreateProduct {\n input := {\n @required\n name: String\n @required\n sku: String\n price: BigDecimal\n @length(min: 0, max: 10)\n tags: TagList\n metadata: MetadataMap\n }\n output := {\n @required\n product: ProductShape\n }\n errors: [ConflictError, ValidationError]\n}\n\n@paginated\n@readonly\n@http(method: \"GET\", uri: \"/products\")\noperation ListProducts {\n input := {\n @httpQuery(\"maxResults\")\n @range(min: 1, max: 100)\n maxResults: Integer\n @httpQuery(\"nextToken\")\n nextToken: String\n @httpQuery(\"category\")\n category: String\n }\n output := {\n @required\n products: ProductList\n nextToken: String\n }\n}\n\nstructure ProductShape {\n @required\n productId: ProductId\n @required\n name: String\n @required\n sku: String\n price: BigDecimal\n tags: TagList\n metadata: MetadataMap\n @required\n createdAt: Timestamp\n updatedAt: Timestamp\n}\n\nlist ProductList {\n member: ProductShape\n}\n\nlist TagList {\n member: String\n}\n\nmap MetadataMap {\n key: String\n value: String\n}\n\n@error(\"client\")\n@httpError(404)\nstructure ProductNotFound {\n @required\n message: String\n productId: ProductId\n}\n\n@error(\"client\")\n@httpError(409)\nstructure ConflictError {\n @required\n message: String\n}\n\n@error(\"client\")\n@httpError(400)\nstructure ValidationError {\n @required\n message: String\n fieldErrors: FieldErrorList\n}\n\nlist FieldErrorList {\n member: FieldError\n}\n\nstructure FieldError {\n @required\n field: String\n @required\n message: String\n}\n```\n\nNotice several things:\n\n1. **Resources are first-class.** ` Product ` is declared as a resource with CRUD operations mapped to it. This isn't just documentation, the code generator uses resource relationships to generate helper methods like ` product.refresh() ` or ` product.delete() `.\n2. **Constraints are in the model.** ` @range(min: 1, max: 100) ` on ` maxResults ` means the generated SDK validates this client-side before making a network call. ` @length(min: 0, max: 10) ` on tags does the same.\n3. **Pagination is declarative.** The ` @paginated ` trait on the service and operation tells the generator to produce auto-pagination helpers. In the TypeScript SDK, this becomes a ` paginateListProducts() ` function that returns an async iterator.\n4. **Errors are typed.** Each error structure maps to a specific HTTP status code. The generated SDK produces typed error classes that the caller can catch specifically."},{"heading":"The Trait System","level":2,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#the-trait-system","content":"Smithy's killer feature is traits. A trait is metadata attached to any shape (structure, operation, service, member) that modifies behavior or adds information. [AWS](<https://aws.com/>) defines hundreds of traits, but you can create your own:\n\n```smithy\n@trait(selector: \"operation\")\nstructure rateLimit {\n @required\n requestsPerSecond: Integer\n burstSize: Integer\n}\n\n@trait(selector: \"structure > member\")\nstructure sensitive {}\n\n@rateLimit(requestsPerSecond: 100, burstSize: 200)\noperation CreateProduct { ... }\n\nstructure UserProfile {\n name: String\n @sensitive\n email: String\n @sensitive\n ssn: String\n}\n```\n\nOur custom ` @rateLimit ` trait tells the SDK generator to add client-side rate limiting. The ` @sensitive ` trait (this one is built-in, actually) tells the generator to redact the value in logs and error messages. These aren't just annotations, they drive code generation."},{"heading":"Custom Validators","level":2,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#custom-validators","content":"Smithy has a validation pipeline that runs on your model before code generation. We wrote custom validators to enforce our API design standards:\n\n```java\npublic final class PaginationValidator extends AbstractValidator {\n @Override\n public List<ValidationEvent> validate(Model model) {\n List<ValidationEvent> events = new ArrayList<>();\n\n for (OperationShape op : model.getOperationShapes()) {\n // Every list operation must be paginated\n if (op.getId().getName().startsWith(\"List\")\n && !op.hasTrait(PaginatedTrait.class)) {\n events.add(danger(op,\n \"List operations must use the @paginated trait. \"\n + \"Operation: \" + op.getId()));\n }\n\n // All operations must have at least one error\n if (op.getErrors().isEmpty()) {\n events.add(warning(op,\n \"Operations should define at least one error type. \"\n + \"Operation: \" + op.getId()));\n }\n }\n\n // All structures with a 'createdAt' must also have 'updatedAt'\n for (StructureShape struct : model.getStructureShapes()) {\n boolean hasCreatedAt = struct.getMember(\"createdAt\").isPresent();\n boolean hasUpdatedAt = struct.getMember(\"updatedAt\").isPresent();\n\n if (hasCreatedAt && !hasUpdatedAt) {\n events.add(danger(struct,\n \"Structures with 'createdAt' must also have 'updatedAt'. \"\n + \"Structure: \" + struct.getId()));\n }\n }\n\n return events;\n }\n}\n```\n\nThis validator runs as part of ` smithy build ` and fails the build if any list operation isn't paginated or if timestamp audit fields are incomplete. It caught three missing pagination configurations in our models within the first week."},{"heading":"Building a Custom SDK Generator","level":2,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#building-a-custom-sdk-generator","content":"The real power comes when you build your own code generator. Smithy's codegen framework gives you a model, a set of integrations, and you produce files. Here's a simplified version of how our TypeScript generator works:\n\n```java\npublic final class TypeScriptCodegenPlugin implements SmithyBuildPlugin {\n\n @Override\n public String getName() {\n return \"typescript-sdk\";\n }\n\n @Override\n public void execute(PluginContext context) {\n Model model = context.getModel();\n ServiceShape service = model.expectShape(\n context.getSettings().getStringMember(\"service\")\n .map(ShapeId::from)\n .orElseThrow(),\n ServiceShape.class\n );\n\n TypeScriptWriter writer = new TypeScriptWriter();\n\n // Generate client class\n generateClient(writer, model, service);\n\n // Generate types for all structures\n for (StructureShape struct : model.getStructureShapes()) {\n if (isInService(model, service, struct)) {\n generateInterface(writer, model, struct);\n }\n }\n\n // Generate operation functions\n for (OperationShape op : getServiceOperations(model, service)) {\n generateOperation(writer, model, op);\n\n // Generate paginator if paginated\n if (op.hasTrait(PaginatedTrait.class)) {\n generatePaginator(writer, model, op);\n }\n }\n\n // Generate error classes\n for (StructureShape error : getServiceErrors(model, service)) {\n generateErrorClass(writer, model, error);\n }\n\n // Write all files\n writer.flushTo(context.getFileManifest());\n }\n}\n```\n\nThe generated TypeScript client looks like this:\n\n```typescript\n// Generated by smithy-typescript-codegen - DO NOT EDIT\nimport { HttpClient, RequestOptions } from './runtime';\n\nexport interface GetProductInput {\n productId: string;\n}\n\nexport interface GetProductOutput {\n product: Product;\n}\n\nexport interface Product {\n productId: string;\n name: string;\n sku: string;\n price?: number;\n tags?: string[];\n metadata?: Record<string, string>;\n createdAt: Date;\n updatedAt?: Date;\n}\n\nexport class ProductNotFoundError extends ServiceError {\n readonly name = 'ProductNotFound';\n readonly statusCode = 404;\n readonly productId?: string;\n\n constructor(message: string, productId?: string) {\n super(message);\n this.productId = productId;\n }\n}\n\nexport class InventoryServiceClient {\n private readonly http: HttpClient;\n\n constructor(config: ClientConfig) {\n this.http = new HttpClient(config);\n }\n\n async getProduct(\n input: GetProductInput,\n options?: RequestOptions\n ): Promise<GetProductOutput> {\n // Client-side validation (from @required trait)\n if (!input.productId) {\n throw new ValidationError('productId is required');\n }\n\n return this.http.request({\n method: 'GET',\n path: `/products/${encodeURIComponent(input.productId)}`,\n ...options,\n });\n }\n\n async *paginateListProducts(\n input: ListProductsInput = {},\n options?: RequestOptions\n ): AsyncGenerator<Product[], void, undefined> {\n let nextToken = input.nextToken;\n do {\n const response = await this.listProducts(\n { ...input, nextToken },\n options\n );\n yield response.products;\n nextToken = response.nextToken;\n } while (nextToken);\n }\n}\n```\n\nThe paginator is automatically generated because ` ListProducts ` has the ` @paginated ` trait. The error classes are generated because the operation declares ` errors: [ProductNotFound, ValidationError] `. The validation logic is generated from the ` @required ` and ` @range ` traits."},{"heading":"The Build Pipeline","level":2,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#the-build-pipeline","content":"Our complete pipeline:\n\n```\nsmithy-model/\n├── model/\n│ ├── inventory.smithy\n│ ├── orders.smithy\n│ └── common.smithy\n├── validators/\n│ └── ApiStandardsValidator.java\n├── smithy-build.json\n└── build/\n ├── typescript/\n ├── python/\n ├── go/\n └── rust/\n```\n\nThe ` smithy-build.json ` orchestrates everything:\n\n```json\n{\n \"version\": \"1.0\",\n \"sources\": [\"model\"],\n \"maven\": {\n \"dependencies\": [\n \"software.amazon.smithy:smithy-model:1.45.0\",\n \"software.amazon.smithy:smithy-openapi:1.45.0\",\n \"com.example:api-validators:1.0.0\"\n ]\n },\n \"plugins\": {\n \"openapi\": {\n \"service\": \"com.example.inventory#InventoryService\",\n \"protocol\": \"aws.protocols#restJson1\"\n },\n \"typescript-sdk\": {\n \"service\": \"com.example.inventory#InventoryService\",\n \"packageName\": \"@example/inventory-sdk\",\n \"packageVersion\": \"2.14.0\"\n },\n \"python-sdk\": {\n \"service\": \"com.example.inventory#InventoryService\",\n \"packageName\": \"example-inventory\"\n }\n }\n}\n```\n\nOne ` smithy build ` command generates four SDKs, an OpenAPI spec, and validates the model against our standards. The generated SDKs are published to internal registries via CI. When an engineer changes the Smithy model and merges, all four SDKs are regenerated, tested against the running service, and published, automatically."},{"heading":"Was It Worth It?","level":2,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#was-it-worth-it","content":"The initial setup took about three weeks, one week learning Smithy, one week building the code generators, one week wiring up CI/CD. Since then:\n\n- Zero drift between SDKs and the actual API\n- New operations go from model change to published SDK in 12 minutes\n- The custom validators have prevented 14 API design inconsistencies before they reached production\n- Engineer onboarding to the SDK is trivial, the generated code is consistent across all services\n\nSmithy's learning curve is steeper than OpenAPI's, and the Java-centric toolchain is a pain if your team isn't JVM-comfortable. But for multi-language SDK generation with type safety and validation baked in, nothing else comes close. The investment pays for itself within a quarter."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#questions","content":""},{"heading":"How does Smithy compare to OpenAPI?","level":3,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#question-1","content":"Smithy is protocol-agnostic (it models APIs, not HTTP endpoints), supports resource-based modeling, has a built-in trait system for extensibility, and generates cleaner SDKs. OpenAPI is more widely adopted, has better tooling for REST documentation, and is the standard for HTTP-specific APIs. Smithy can generate OpenAPI specs from its models."},{"heading":"Can I use Smithy for non-AWS projects?","level":3,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#question-2","content":"Absolutely. Smithy is an open-source project under Apache 2.0. While AWS uses it internally for all their services, the language and tooling are completely general-purpose. You can define any API, use any protocol binding, and generate SDKs for any language."},{"heading":"What languages can Smithy generate SDKs for?","level":3,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#question-3","content":"AWS officially maintains generators for TypeScript, Python, Java, Go, Rust, C++, Swift, Kotlin, Ruby, and PHP. The community has generators for C# and Dart. You can also build custom generators using the smithy-codegen framework."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#article-tips","content":"1. **LuchoTip:** Smithy's ` @paginated ` trait is far more powerful than it looks. Define it once on your list operations and the SDK generator can automatically produce both sync iterators and async pagination helpers. Don't roll your own pagination pattern.\n2. **LuchoTip:** Use Smithy's ` @examples ` trait on every operation. They serve as documentation, but more importantly, the code generator can turn them into integration test scaffolding that validates your SDK against real API responses.\n3. **LuchoTip:** When writing custom validators, always implement ` AbstractValidator<Shape> ` rather than checking shapes manually in a plugin. Validators get composed correctly in the validation pipeline and their diagnostics show up in IDEs with proper source locations.\n\n- smithy\n- api-design\n- aws\n- sdk-generation\n- code-generation\n- openapi\n- protocol-buffers\n- idl\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/smithy-api-modeling-aws-sdk-generation"},{"text":"Smithy","url":"https://smithy.io/"},{"text":"AWS","url":"https://aws.com/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation"},"headline":"Smithy: AWS's API Modeling Language and Building Your Own SDK Generator","description":"An examination of Smithy, AWS's protocol-agnostic API modeling language, its code generation pipeline, custom validators, trait system, and how to build a custom SDK generator from a Smithy model.","datePublished":"2025-11-07","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"API Design","keywords":"smithy, api-design, aws, sdk-generation, code-generation, openapi, protocol-buffers, idl","wordCount":1384,"timeRequired":"PT8M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/smithy-api-modeling-aws-sdk-generation"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Smithy: AWS's API Modeling Language and Building Your Own SDK Generator","item":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"How does Smithy compare to OpenAPI?","acceptedAnswer":{"@type":"Answer","text":"Smithy is protocol-agnostic (it models APIs, not HTTP endpoints), supports resource-based modeling, has a built-in trait system for extensibility, and generates cleaner SDKs. OpenAPI is more widely adopted, has better tooling for REST documentation, and is the standard for HTTP-specific APIs. Smithy can generate OpenAPI specs from its models."}},{"@type":"Question","name":"Can I use Smithy for non-AWS projects?","acceptedAnswer":{"@type":"Answer","text":"Absolutely. Smithy is an open-source project under Apache 2.0. While AWS uses it internally for all their services, the language and tooling are completely general-purpose. You can define any API, use any protocol binding, and generate SDKs for any language."}},{"@type":"Question","name":"What languages can Smithy generate SDKs for?","acceptedAnswer":{"@type":"Answer","text":"AWS officially maintains generators for TypeScript, Python, Java, Go, Rust, C++, Swift, Kotlin, Ruby, and PHP. The community has generators for C# and Dart. You can also build custom generators using the smithy-codegen framework."}}]}]}],"contentHash":"78cd2ee7fc5265e3ff8d0e004113d787f1345099eaec7bb3d2bc18df37ef8284","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/smithy-api-modeling-aws-sdk-generation","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":64130,"contentBytes":13886}}
