You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 6b7d9a6
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: src/content/articles/openapi-spec-documentation-sdk-generation-validation/index.mdx
+8-7Lines changed: 8 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,15 +13,15 @@ featured: true
13
13
14
14
Most teams I work with treat OpenAPI specs as __output__ — something you generate from existing code and call "documentation." That's archaeology, not specification. Meanwhile, their hand-written docs in Confluence are perpetually out of date, their manually coded SDKs break when the API changes, and validation logic differs between the gateway and the backend.
15
15
16
-
The real value of OpenAPI emerges when you flip the relationship. The spec becomes the source of truth that drives your code, not the other way around. When the spec drives everything — validation middleware, CI checks, generated clients — consistency becomes automatic. The spec can't disagree with your validation because your validation __reads__ the spec. Documentation can't drift because it's generated from the same source.
16
+
The real value of OpenAPI emerges when you flip the relationship. The spec becomes the source of truth that drives your code, not the other way around. When the spec drives everything — validation middleware, CI checks, generated clients — consistency becomes automatic. The spec can't disagree with your validation because your validation _reads_ the spec. Documentation can't drift because it's generated from the same source.
17
17
18
18
That's the theory. Here's how it works in practice, starting with the two areas that pay off immediately: request validation and CI automation.
19
19
20
20
## Request Validation: Where the Spec Earns Its Keep
21
21
22
22
Validation is where OpenAPI specs prove their worth. Instead of writing validation logic by hand — checking types, verifying formats, ensuring required fields exist — you derive it directly from the spec. The same schema that defines your API contract also enforces it at runtime.
23
23
24
-
The key is understanding what schema validation actually does. It handles __structural__ correctness: rejecting requests where `quantity` is a string instead of an integer, where `email` doesn't match email format, where required fields are missing. It won't reject a request where `customerId` is a valid UUID that doesn't exist in your database. That's business validation, and you still need it.
24
+
The key is understanding what schema validation actually does. It handles _structural_ correctness: rejecting requests where `quantity` is a string instead of an integer, where `email` doesn't match email format, where required fields are missing. It won't reject a request where `customerId` is a valid UUID that doesn't exist in your database. That's business validation, and you still need it.
25
25
26
26
In Node.js/Express, `express-openapi-validator` is the standard choice. Point it at your spec file, and it automatically validates request bodies, query parameters, path parameters, and headers against your schemas:
27
27
@@ -54,10 +54,11 @@ Code: Express middleware that validates requests against your OpenAPI spec.
54
54
55
55
The `validateResponses: true` option deserves attention. Enable it in development and staging — it catches cases where your implementation returns data that doesn't match your spec. That's an early warning that spec and code have drifted apart. You might disable it in production for performance, but keeping it on during development catches bugs before you ship.
56
56
57
-
Other frameworks have equivalent solutions. If you're in Python, FastAPI handles this automatically — your Pydantic type hints __are__ your validation schema, and FastAPI generates an OpenAPI spec from them. You get validation and documentation from the same source without any additional configuration. Rails has `committee` (Rack middleware that validates against a spec file), Laravel has `spectator`, Go has `kin-openapi` for Echo, Chi, and Gin. The pattern is the same across all of them: point middleware at your spec, let it reject malformed requests before they hit your business logic.
57
+
Other frameworks have equivalent solutions. If you're in Python, FastAPI handles this automatically — your Pydantic type hints _are_ your validation schema, and FastAPI generates an OpenAPI spec from them. You get validation and documentation from the same source without any additional configuration. Rails has `committee` (Rack middleware that validates against a spec file), Laravel has `spectator`, Go has `kin-openapi` for Echo, Chi, and Gin. The pattern is the same across all of them: point middleware at your spec, let it reject malformed requests before they hit your business logic.
58
58
59
59
<Table
60
-
variant="vertical-column-delineation-table"
60
+
variant="grid-and-accent-header-table"
61
+
fullWidth={false}
61
62
content={{
62
63
thead: {
63
64
th: ['Framework', 'Library', 'Approach'],
@@ -66,19 +67,19 @@ Other frameworks have equivalent solutions. If you're in Python, FastAPI handles
66
67
tr: [
67
68
{
68
69
th: 'Express/Node',
69
-
td: ['`express-openapi-validator`', 'Middleware validates against spec file'],
70
+
td: ['<span class="font-mono">express-openapi-validator</span>', 'Middleware validates against spec file'],
70
71
},
71
72
{
72
73
th: 'FastAPI/Python',
73
74
td: ['Built-in with Pydantic', 'Type hints become validation schema'],
74
75
},
75
76
{
76
77
th: 'Rails',
77
-
td: ['`committee`', 'Rack middleware validates against spec'],
78
+
td: ['<span class="font-mono">committee</span>', 'Rack middleware validates against spec'],
78
79
},
79
80
{
80
81
th: 'Go',
81
-
td: ['`kin-openapi`', 'Middleware for Echo, Chi, Gin'],
82
+
td: ['<span class="font-mono">kin-openapi</span>', 'Middleware for Echo, Chi, Gin'],
Copy file name to clipboardExpand all lines: src/content/articles/openapi-spec-documentation-sdk-generation-validation/pdf.mdx
+64-33Lines changed: 64 additions & 33 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -32,19 +32,19 @@ If you've worked with OpenAPI specs before, you know they can get unwieldy fast.
32
32
An OpenAPI spec has four top-level sections that matter:
33
33
34
34
<List
35
-
variant="check-icons-list"
35
+
variant="card-grid-list"
36
36
items={[
37
37
{
38
-
text: '`info` for metadata',
38
+
lead: '<span class="font-mono">info</span> for metadata',
39
39
},
40
40
{
41
-
text: '`servers` for environment URLs',
41
+
lead: '<span class="font-mono">servers</span> for environment URLs',
42
42
},
43
43
{
44
-
text: '`paths` for your actual endpoints',
44
+
lead: '<span class="font-mono">paths</span> for your actual endpoints',
45
45
},
46
46
{
47
-
text: '`components` for reusable definitions',
47
+
lead: '<span class="font-mono">components</span> for reusable definitions',
48
48
},
49
49
]}
50
50
/>
@@ -184,8 +184,9 @@ Code: Using allOf to compose shared base properties into entity schemas.
184
184
For polymorphic types — where a field can be one of several shapes — use `oneOf` with a discriminator. Payment methods are the classic example: credit cards have `last4` and `expiryMonth`, bank transfers have `routingNumber` and `accountNumber`, PayPal has `email`. The discriminator tells parsers which schema applies based on a `type` field.
185
185
186
186
<Table
187
-
variant="vertical-column-delineation-table"
187
+
variant="numbered-review-table"
188
188
content={{
189
+
figure: 'OpenAPI schema design patterns and when to use them',
189
190
thead: {
190
191
th: ['Pattern', 'When to Use'],
191
192
},
@@ -196,11 +197,11 @@ For polymorphic types — where a field can be one of several shapes — use `on
196
197
td: ['Create/update/read operations have different field requirements'],
td: ['A field can be one of several distinct types with different structures'],
205
206
},
206
207
],
@@ -261,7 +262,7 @@ The workflow looks like this: design the API in your spec editor, review with st
261
262
You'll want a decent editor for spec-first work. Here are the main options:
262
263
263
264
<Table
264
-
variant="vertical-column-delineation-table"
265
+
variant="grid-and-accent-header-table"
265
266
content={{
266
267
thead: {
267
268
th: ['Editor', 'Best For'],
@@ -361,19 +362,22 @@ Once you have an OpenAPI spec, generating documentation is almost trivially easy
361
362
The three main options cover different use cases:
362
363
363
364
<List
364
-
variant="check-icons-list"
365
+
variant="chevron-list"
366
+
classes={{
367
+
wrapper: "sm:ml-6 sm:mr-12 mb-8",
368
+
}}
365
369
items={[
366
370
{
367
371
lead: 'Redoc',
368
-
text: 'generates static HTML from your spec. You get a three-panel layout (navigation, content, code samples), search, and nested schema expansion. It\'s a single HTML file you can host anywhere — GitHub Pages, S3, your CDN. No backend required. This is my default choice for most projects because it\'s zero-maintenance once deployed.',
372
+
text: 'Generates static HTML from your spec. You get a three-panel layout (navigation, content, code samples), search, and nested schema expansion. It\'s a single HTML file you can host anywhere — GitHub Pages, S3, your CDN. No backend required. This is my default choice for most projects because it\'s zero-maintenance once deployed.',
369
373
},
370
374
{
371
375
lead: 'Swagger UI',
372
-
text: 'provides interactive documentation where developers can make live requests against your API. The "Try it out" button is genuinely useful for exploration and debugging. The tradeoff: you need to think about authentication, CORS, and whether you want people hitting your production API from the docs.',
376
+
text: 'Provides interactive documentation where developers can make live requests against your API. The "Try it out" button is genuinely useful for exploration and debugging. The tradeoff: you need to think about authentication, CORS, and whether you want people hitting your production API from the docs.',
373
377
},
374
378
{
375
379
lead: 'Stoplight Elements',
376
-
text: 'is a React component library for embedding documentation in an existing portal or documentation site. If you\'re building a developer portal with tutorials, guides, and API reference together, Elements lets you integrate the API docs natively rather than linking out to a separate site.',
380
+
text: 'A React component library for embedding documentation in an existing portal or documentation site. If you\'re building a developer portal with tutorials, guides, and API reference together, Elements lets you integrate the API docs natively rather than linking out to a separate site.',
377
381
},
378
382
]}
379
383
/>
@@ -443,9 +447,13 @@ responses:
443
447
```
444
448
Code: Named examples for different order states.
445
449
450
+
These individual techniques — rich descriptions, multiple examples, error scenarios — compound when applied consistently across your spec. The table below summarizes which documentation elements to prioritize and what each should contain, so you can audit an existing spec or build a new one with the right coverage from the start.
451
+
446
452
<Table
447
-
variant="vertical-column-delineation-table"
453
+
variant="numbered-review-table"
454
+
fullWidth={false}
448
455
content={{
456
+
figure: 'Documentation elements that improve developer experience.',
449
457
thead: {
450
458
th: ['Documentation Element', 'What to Include'],
451
459
},
@@ -469,7 +477,6 @@ Code: Named examples for different order states.
469
477
},
470
478
],
471
479
},
472
-
figure: 'Documentation elements that improve developer experience.',
473
480
}}
474
481
/>
475
482
@@ -490,25 +497,35 @@ The good news is that SDK generation has matured significantly. With the right g
490
497
The SDK generator landscape breaks down into three categories: comprehensive multi-language tools, language-specific tools with better output, and types-only generators.
text: '(the open-source fork of Swagger Codegen) supports 50+ languages and is the default choice for teams that need SDKs in multiple languages from the same spec. The TypeScript output is decent, the Python output is usable, and you get consistent behavior across languages. The tradeoff is that "decent" and "usable" aren\'t "great"—the generated code tends toward verbosity and doesn\'t always follow language idioms.',
508
+
lead: 'OpenAPI Generator (the open-source fork of Swagger Codegen)',
509
+
text: 'Supports 50+ languages and is the default choice for teams that need SDKs in multiple languages from the same spec. The TypeScript output is decent, the Python output is usable, and you get consistent behavior across languages. The tradeoff is that "decent" and "usable" aren\'t "great"—the generated code tends toward verbosity and doesn\'t always follow language idioms.',
498
510
},
499
511
{
500
512
lead: 'Orval',
501
-
text: 'focuses exclusively on TypeScript but produces notably better output. It generates React Query hooks, Zod validation schemas, and MSW mock handlers out of the box. If your consumers are React developers, Orval\'s output feels like code they\'d actually write.',
513
+
text: 'Focuses exclusively on TypeScript but produces notably better output. It generates React Query hooks, Zod validation schemas, and MSW mock handlers out of the box. If your consumers are React developers, Orval\'s output feels like code they\'d actually write.',
502
514
},
503
515
{
504
-
lead: 'openapi-typescript',
505
-
text: 'takes a different approach: it generates only TypeScript types, no runtime code. You get interfaces for your request/response bodies and use your own HTTP client. This is the lightest-weight option and works well when you want type safety without adopting a generated SDK wholesale.',
text: 'Takes a different approach: it generates only TypeScript types, no runtime code. You get interfaces for your request/response bodies and use your own HTTP client. This is the lightest-weight option and works well when you want type safety without adopting a generated SDK wholesale.',
506
518
},
507
519
]}
508
520
/>
509
521
522
+
These tools solve different problems, so the right choice depends less on raw feature count and more on who will consume the generated code. A frontend-heavy team may prioritize React Query integration and ergonomic TypeScript output, while a platform team supporting multiple languages may accept less idiomatic clients in exchange for broader coverage. The comparison below makes those tradeoffs explicit.
523
+
510
524
<Table
511
-
variant="vertical-column-delineation-table"
525
+
variant="grid-and-accent-header-table"
526
+
classes={{
527
+
tbody: '!text-content',
528
+
}}
512
529
content={{
513
530
thead: {
514
531
th: ['Generator', 'Languages', 'Best For'],
@@ -636,7 +653,7 @@ For Python, FastAPI handles this automatically if you're using Pydantic models.
636
653
Other ecosystems have their own solutions:
637
654
638
655
<Table
639
-
variant="vertical-column-delineation-table"
656
+
variant="criteria-highlight-table"
640
657
content={{
641
658
thead: {
642
659
th: ['Framework', 'Library', 'Approach'],
@@ -645,23 +662,23 @@ Other ecosystems have their own solutions:
645
662
tr: [
646
663
{
647
664
th: 'Rails',
648
-
td: ['`committee`', 'Rack middleware that validates against OpenAPI specs'],
665
+
td: ['<span class="font-mono">committee</span>', 'Rack middleware that validates against OpenAPI specs'],
649
666
},
650
667
{
651
668
th: 'Laravel',
652
-
td: ['`spectator`', 'Request/response validation with PHPUnit integration'],
669
+
td: ['<span class="font-mono">spectator</span>', 'Request/response validation with PHPUnit integration'],
653
670
},
654
671
{
655
672
th: '.NET',
656
-
td: ['`Microsoft.AspNetCore.OpenApi`', 'Built-in validation in ASP.NET Core 9+; earlier versions use `NSwag`'],
673
+
td: ['<span class="font-mono">Microsoft.AspNetCore.OpenApi</span>', 'Built-in validation in ASP.NET Core 9+; earlier versions use <span class="font-mono">NSwag</span>'],
657
674
},
658
675
{
659
676
th: 'Go',
660
-
td: ['`kin-openapi`', 'Middleware for Echo, Chi, Gin; validates requests and responses'],
677
+
td: ['<span class="font-mono">kin-openapi</span>', 'Middleware for Echo, Chi, Gin; validates requests and responses'],
661
678
},
662
679
{
663
680
th: 'Spring Boot',
664
-
td: ['`springdoc-openapi`', 'Code-first with automatic spec generation and validation'],
681
+
td: ['<span class="font-mono">springdoc-openapi</span>', 'Code-first with automatic spec generation and validation'],
665
682
},
666
683
],
667
684
},
@@ -714,7 +731,8 @@ Code: Error handler that transforms technical validation errors into readable me
714
731
It's important to understand the boundaries of schema validation:
715
732
716
733
<Table
717
-
variant="vertical-column-delineation-table"
734
+
variant="grid-and-accent-header-table"
735
+
fullWidth={false}
718
736
content={{
719
737
thead: {
720
738
th: ['Validation Catches', 'Validation Misses'],
@@ -854,22 +872,32 @@ Breaking changes are the silent killer of API reliability. Someone adds a requir
854
872
`openapi-diff` compares two spec versions and flags incompatible changes:
855
873
856
874
<List
857
-
variant="check-icons-list"
875
+
variant="colored-marker-list"
876
+
size={3}
877
+
classes={{
878
+
wrapper: "sm:ml-6 sm:mr-12 mb-6",
879
+
text: "pt-1",
880
+
}}
858
881
items={[
859
882
{
860
883
text: 'Removed endpoints',
884
+
color: "bg-danger-offset",
861
885
},
862
886
{
863
887
text: 'Removed or renamed fields',
888
+
color: "bg-danger-offset",
864
889
},
865
890
{
866
891
text: 'New required fields in request bodies',
892
+
color: "bg-danger-offset",
867
893
},
868
894
{
869
895
text: 'Changed field types',
896
+
color: "bg-danger-offset",
870
897
},
871
898
{
872
899
text: 'Narrowed enum values',
900
+
color: "bg-danger-offset",
873
901
},
874
902
]}
875
903
/>
@@ -881,15 +909,18 @@ Run it in PR checks with `--fail-on-incompatible` to block merges that would bre
881
909
Contract tests verify your implementation actually matches your spec. Dredd and Schemathesis are the main tools:
882
910
883
911
<List
884
-
variant="check-icons-list"
912
+
variant="chevron-list"
913
+
classes={{
914
+
wrapper: "sm:ml-6 sm:mr-12 mb-6",
915
+
}}
885
916
items={[
886
917
{
887
918
lead: 'Dredd',
888
-
text: 'reads your spec, generates requests for each endpoint, hits your running server, and validates responses against the schema. It\'s deterministic — same spec produces same tests.',
919
+
text: 'Reads your spec, generates requests for each endpoint, hits your running server, and validates responses against the schema. It\'s deterministic — same spec produces same tests.',
889
920
},
890
921
{
891
922
lead: 'Schemathesis',
892
-
text: 'takes a property-based testing approach. It generates random valid inputs based on your schemas and looks for edge cases that break your implementation. It\'s better at finding bugs but less predictable.',
923
+
text: 'Takes a property-based testing approach. It generates random valid inputs based on your schemas and looks for edge cases that break your implementation. It\'s better at finding bugs but less predictable.',
0 commit comments