I build cloud-based systems for startups and enterprises. My background in operations gives me a unique focus on writing observable, reliable software and automating maintenance work.
I love learning and teaching about Amazon Web Services, automation tools such as Ansible, and the serverless ecosystem. I most often write code in Python, TypeScript, and Rust.
B.S. Applied Networking and Systems Administration, minor in Software Engineering from Rochester Institute of Technology.
My favorite part of working in GraphQL isn’t any of the runtime stuff. Applications I’ve worked on haven’t suffered from the over-fetching so commonly pointed to by GraphQL advocates as an inefficiency. Having a schema definition that is easily maintained and written without much of a learning curve is what makes GraphQL great. Recently I discovered Typespec, which has a familiar struct-like Schema Definition Language (SDL), decorators, and custom transformers. It also doesn’t require runtime application changes – you can use it for defining schemas without making any application changes.
There are already schema definition languages galore either standalone or as part of larger tools. XML schema definitions, JSONSchema, OpenAPI/Swagger, Smithy, Avro, Coral, Protobuf, and Thrift are just some that come to mind. Each comes with its own tooling and (sometimes) wire format. A good SDL must have a strict schema, cross-language support, and incremental adoptability.
To have something concrete to look at, let’s specialize a string into UUIDv4 format. Trust me on the regex, but this will match 5f881007-5450-4950-ac19-77ae8cb93be4
. With this, we’ll be able to specify a UUID
type and it will bring along the length and regex rules. The regex having character counts technically makes the length specification redundant, but let’s be extra specific.
@minLength(32)
@maxLength(32)
@pattern(
"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
"Must be a UUID string."
)
scalar UUID extends string;
Building on that, here’s the Typespec schema for an error type that would be at home in almost any application.
@error
model ErrorDetail {
@minLength(4)
@maxLength(32)
@pattern(
"^[A-Z][A-Z0-9_]+[A-Z0-9]$",
"Must be 4-32 characters long in SCREAMING_SNAKE_CASE."
)
code: string;
message?: string;
}
@error
model NotFoundDetail extends ErrorDetail {
code: "NOT_FOUND";
request_id: UUID
}
An error might be as sparse as {"code": "OH_NO"}
or have an optional explanatory message. With extends
, ErrorDetail
can have request ID (using our UUID type from earlier) in addition to code
and message
to simplify troubleshooting.
So far we’ve only used Typespec’s basic struct-style definitions. What makes Typespec great is that its core is protocol-agnostic: we could use it for a REST API, an event, or a log message format. To use Typespec you’ll need to export to a format that can be enforced by your application and used by frameworks as you build. I use the OpenAPI 3/Swagger export feature with the REST module to add paths, HTTP verbs, and status codes. We can wrap NotFoundDetail
with a type that includes an HTTP status code.
@error
model NotFound {
@statusCode _: 404;
body: NotFoundDetail;
}
Typespec can then render this to OpenAPI 3. Then we can use any tool that accepts OpenAPI to generate docs, clients, and validators.
Let’s put this together with an API to return information about all the different ISO 3166 countries and sub-national divisions (provinces, states, and so on). We can send in a 3-letter code and get back all the sub-divisions of that country, such as GET /v1/regions/USA
to receive a list of states.
import "@typespec/http";
using TypeSpec.Http;
@service({
title: "Nations",
})
@server("http://localhost:3000", "Local dev")
namespace Nations;
@error
model ErrorDetail {
@minLength(4)
@maxLength(32)
@pattern(
"^[A-Z][A-Z0-9_]+[A-Z0-9]$",
"Must be 4-32 characters long in SCREAMING_SNAKE_CASE."
)
code: string;
message?: string;
}
@error
model NotFoundDetail extends ErrorDetail {
code: "NOT_FOUND";
}
@error
model NotFound {
@statusCode _: 404;
body: NotFoundDetail;
}
@route("/v1/regions")
namespace Region {
model Country {
@minLength(3)
@maxLength(3)
code: string;
name: string;
divs: Subdivision;
}
model Subdivision {
name: string;
code: string;
div_type: string;
}
model Countries {
countries: Country[];
}
@get
op get(@path code: string): {
@statusCode _: 200;
body: {
country?: Country;
};
} | NotFound;
}
The below YAML version of the same specification has all our properties converted to OpenAPI, and the namespace Region
has been applied to all the types inside the namespace. Also note that our above op get
has a union operator, |
, that shows up in the OpenAPI as the 200 and 404 status codes and responses.
# compiled with `tsp compile . --option "@typespec/openapi3.file-type=yaml"`
openapi: 3.0.0
info:
title: Nations
version: 0.0.0
servers:
- url: http://localhost:3000
description: Local dev
variables: {}
paths:
/v1/regions/{code}:
get:
operationId: Region_get
parameters:
- name: code
in: path
required: true
schema:
type: string
responses:
'200':
description: The request has succeeded.
content:
application/json:
schema:
type: object
properties:
body:
type: object
properties:
country:
$ref: '#/components/schemas/Region.Country'
required:
- body
'404':
description: The server cannot find the requested resource.
content:
application/json:
schema:
$ref: '#/components/schemas/NotFound'
components:
schemas:
ErrorDetail:
type: object
required:
- code
properties:
code:
type: string
minLength: 4
maxLength: 32
pattern: ^[A-Z][A-Z0-9_]+[A-Z0-9]$
message:
type: string
NotFound:
type: object
required:
- body
properties:
body:
$ref: '#/components/schemas/NotFoundDetail'
NotFoundDetail:
type: object
required:
- code
properties:
code:
type: string
enum:
- NOT_FOUND
allOf:
- $ref: '#/components/schemas/ErrorDetail'
Region.Country:
type: object
required:
- code
- name
- divs
properties:
code:
type: string
minLength: 3
maxLength: 3
name:
type: string
divs:
$ref: '#/components/schemas/Region.Subdivision'
Region.Subdivision:
type: object
required:
- name
- code
- div_type
properties:
name:
type: string
code:
type: string
div_type:
type: string
Like a bad ORM, a bad SDL can overwhelm its own usefulness with impedance mismatch for your language or workload. JSONSchema is great at specifying payloads, but won’t help you design something like a RESTful API because it lacks knowledge of HTTP verbs and paths. gRPC’s assumption of an RPC-style system makes modeling evented systems like message buses difficult. Typespec mainly seems focused on HTTP APIs, but its modular nature and output plugins mean it’s extensible to other use cases.
Smithy is AWS’ entry into the SDL arena and attempts to learn lessons from the older Coral SDL. It suffers from a limited tooling ecosystem because so few outside of Amazon have adopted it. In contrast, Typespec users can leverage existing OpenAPI-driven tools for client generation, docs, and more.
Back to GraphQL, and the promise to solve over-fetching. In GraphQL-based applications I’ve seen devs copy every field in the schema to their requests. To best address over-fetching, use a query fragment assembling system like Apollo layers on top of your web/mobile application. Otherwise, each component in your web UI may develop its own slightly different set of fields and duplicate requests regardless of your SDL.
Typespec doesn’t attempt to solve run-time problems beyond validation, but within the project’s stated goals the team has put together an elegant SDL and chosen good integrations points with the broader ecosystem.