I’m working on a library with a similar approach for Node/TypeScript. Obviously the language and platform semantics differ (and unlike several TypeScript API libraries I will be intentionally avoiding decorators because TS/TC39 decorators differ), but it’s nice to see a similar approach validated on a platform with a better reputation for thoughtful design.
I have built another version of this, internal for a previous employer, and it’s been proven successful. It’s really great to have API boundary validation, automatic documentation and static type validation all from a single, simple and declarative source of truth. And I’ve seen first hand how it can improve developer productivity and confidence, project maintainability, and API user experience.
In my experience, Python has a better foundation for this kind of approach: better underlying libraries available in the ecosystem, more likely to work well together. I’ve taken a lot of inspiration from that and hope to at least set a better example for the Node/TS world.
The client can do `makeRequest('createProduct', ...)` and the Express server can use `registerEndpoint('createProduct', ...)` which must adhere to the schema defined above.
TypeScript's structural typing helps a lot here.
Of course, I can "lose" typing through things like raw SQL queries (working on a mini db helper library to help with that), but so I'll have to validate that the endpoints return what they say they do through tests (probs using io-ts).
I'm just getting started (coming from Rails). Not sure what the "correct" way to do this is, but my approach works so far. Would also be nice to have a generic way to create REST endpoints for a given resource, but meh.
Let me give you a head start: what you want in TypeScript is type guards. You pair runtime validation with type refinement, and if you start with good composable primitives, you can declare runtime structures and get compile time safety for “free”. That’s how io-ts and the various other libraries in that space work.
If you combine that with generics on whatever function defines your routes, you can validate API boundaries and business logic boundaries in the compiler. This applies to any boundary (I could link to Gary Bernhardt on that topic but I’m on phone), and you can do it as generically (like you said, io-ts) or specifically (take a look at zapatos for type safe SQL) as you like.
For HTTP boundaries, I’ll drop some pseudocode of how I’m doing this in my library:
type HTTPVerb =
| 'delete'
| 'get'
| 'head'
| 'options'
| 'patch'
| 'post'
| 'put'
| 'trace';
interface HTTPRequest {
readonly method: HTTPVerb;
// ...
}
type HTTPRequestDecoder<I> = (request: HTTPRequest) => I;
type HTTPResponseEncoder<O, R> = (output: O) => R;
type HTTPRoute = IrrelevantForThisPost;
type Handler<I, O> = (input: I) => O;
type HTTPRouteFactory = <I, O, R>(
path: string,
decoder: HTTPRequestDecoder<I>,
handler: Handler<I, O>,
encoder: HTTPResponseEncoder<O, R>
) => HTTPRoute;
type Route = {
readonly [K in HTTPVerb]: HTTPRouteFactory;
};
Fwiw I didn’t type all of it on my phone, I copypasta’ed from a draft blog post then manually indented lol. The approach you’re showing makes sense and it’s not unlike how I’d approach a one off/internal project where I know the routes are all defined in a predictable structure. The benefit of the pseudo types I shared is that the only predetermined structure is the route factory call. It only needs to know input output and serialization types. It can be extended to use TS 4.1 template literal types to validate path parameters. But it’s application agnostic.
I have built another version of this, internal for a previous employer, and it’s been proven successful. It’s really great to have API boundary validation, automatic documentation and static type validation all from a single, simple and declarative source of truth. And I’ve seen first hand how it can improve developer productivity and confidence, project maintainability, and API user experience.
In my experience, Python has a better foundation for this kind of approach: better underlying libraries available in the ecosystem, more likely to work well together. I’ve taken a lot of inspiration from that and hope to at least set a better example for the Node/TS world.