This project is a WIP but the core implementation works.
There is outstanding work on the API in Javascript client function names, which do not indicate request type ('GET', 'POST'). Non-'GET' routes must be defined manually using FastAPI routes.
Non-GET routes that use Pydantic types as required function parameters unpack request bodies into JSON validated by the Pydantic type of the parameters. This is the behavior of FastRPC and the Typescript client methods. The outstanding work here is to automatically name non-GET routes.
A possible killer feature would be merging multiple endpoints with the same name (e.g.
GET /api/records/{record_id}
andGET /api/records
) into one Javascript client methodpythonClient.records(...)
whererecord_id
is optional, hitting different endpoints based on parameters provided (which is how web routing works). Currently, each web endpoint has one client method.TODO: add an option
regenerateSchemaOnlyWhenChanges
. WhenTrue
, theopenapi.json
schema file is only written to if it changes, rather than on every save, in case filesystem modified dates are considered useful.This project requires packaging and is not on Pypi.
The hardest part about "full-stack" web development may not be understanding both frontend and backend languages and frameworks. In some cases, the biggest hurdle is routing and networking.
FastRPC is a thin abstraction over FastAPI that turns the web server framework into an RPC ("remote procedure call") framework.
FastRPC_Demo.mp4
Notice in the above video:
- The Typescript client knows what the input to the RPC function should be.
- Whenever the Python server reloads (on save), the Typescript code-gen takes place and the Typescript client is immediately type-safe.
- All Pydantic input/output types that are part of the RPC endpoints can be imported into Typescript (again, code-gen on every save).
- The time it takes from saving the Python project to the Typescript LSP being fully aware of the client properties is around 4 seconds.
The Python web framework Flask has used the dead-simple route decorator pattern pattern since 2004:
from flask import Flask
app = flask.Flask(__name__)
@app.get('/api/getrecords/<artist>/<int:year>')
def get_records(artist, year):
return [...]
Most routers have mini-languages for routes, and Flask's ensures that the year
parameter is passed to get_records
as an int
, and that a non-integer route won't hit this endpoint, taking advantage of types.
FastAPI simplifies the routing mini-language by inferring types instead from Python and Pydantic type annotations during runtime, again ensuring endpoints are accessed with the desired parameter types:
from fastapi import FastAPI
from dataclasses import dataclass
app=FastAPI()
@app.get('/api/getrecords/{artist}/{year}')
def get_records(artist: str, year: int) -> Record[]:
return [...]
@dataclass
class Record:
title: str
artist: str
year: int
A Python programmer may not want to define or annotate types, and in particular, annotating a web endpoint's return value could be a total waste of time, since it is not called from Python anyway. That is, you won't benefit from your IDE's knowledge that:
[record.title for record in get_records('The Beatles', '1970')]
is valid Python if you're really calling get_records
from Javascript using fetch('/api/getrecords/The%20Beatles/1970')
.
FastAPI requires type annotation in exchange for simple and robust endpoint routing, but the annotations yield much more. FastAPI has built-in support for OpenAPI and can both export an OpenAPI spec (in json or yaml) and host a full Swagger UI that documents and tests your endpoints.
By consuming an OpenAPI spec with a Typescript code-gen tool called openapi-typescript-sdk-generator
, we create a type-aware pythonClient
object in Typescript that can call all our Python endpoints.
With FastRPC, your Typescript LSP will know that the following is valid Typescript:
import { pythonClient } from 'fastrpc' pythonClient.getRecords({artist: 'The Beatles', year: 1970}).map(record => record.title)Your IDE wil autocomplete
record.title
and complain about extraneous arguments!
Exporting an openapi.json
file is so fast that we can do it on every live-reload of the FastAPI server. Consuming the openapi.json
file to code-gen a type-aware pythonClient
SDk in Typescript is so fast that we can do it every time openapi.json
is generated. We use npm-watch
with runOnChangeOnly
to observe changes in openapi.json
.
As soon as we save our Python server definition, our Typescript SDK object is aware of the routes (as client methods), route parameters and their types (signatures of those methods), and return types of those routes (as return types of methods), even if those return types are classes containing data structures. All in exchange for type annotating Python functions!
If we have a Javascript SDK that can call Python functions without worrying about routing web requests, why manually define routes at all?
FastRPC takes away route definitions in route decorators so that developers don't have to think about calling routes via web requests. That is,
app = FastAPI()
@app.get('/api/getrecords/{artist}/{year}')
def get_records(artist: str, year: int) -> Record[]:
...
becomes
app = FastRPC()
@app.getRPC
def get_records(artist: str, year: int) -> Record[]:
...
Internally, FastRPC defines a route by introspecting the function's name and signature (/get_records/{artist}/{year}
in this case).
With this decorator and type annotations, our Typescript client immediately knows that the following is valid:
pythonClient.get_records({artist: 'The Beatles', year: 1970}).map(record => record.title)
Any Python keyword parameters (with a default value) will automatically become optional parameters in the Tpescript client.
Internally, they become query parameters:
@app.getRPC
def get_records(artist: str, year: int, format: str = 'cd', remastered: bool = False) -> Record[]:
...
will allow the following Typescript function calls:
pythonClient.get_records({ artist: 'The Beatles', year: 1970 })
// becomes `fetch('/api/getrecords/The%20Beatles/1970')`
// then becomes `get_records(artist='The Beatles', year=1970)`
pythonClient.get_records({ artist: 'The Beatles', year: 1970, format: 'vinyl', remastered: true })
// becomes `fetch('/api/getrecords/The%20Beatles/1970?format=vinyl&remastered=true')``
// then becomes `get_records(artist='The Beatles', year=1970, format='vinyl', remastered=True)`
Internally, FastRPC creates a fully-functional FastAPI router. FastRPC is an extremely thin layer, combined with Typescript codegen for an SDK.
This means the FastAPI Swagger UI is absolutely accessible for a FastRPC server, and a FastRPC server can absolutely be used with or without a Javascript client object.
A well-designed web API has many very similar routes:
GET /api/records
POST /api/records
GET /api/records/{record_id}
DELETE /api/records/{record_id}
and by using Python function names to construct routes, we necessarily lose this functionality, since we can't name multiple functions the same thing (we actually could but we won't). There are two disadvantages to mangling our router with function names:
- The Javascript client's auto-generated function names may need to be strange, e.g.
pythonClient.getRecords
andpythonClient.getRecords__record_id
. - The internal routes will lack the clarity of a manually-defined web API. The FastRPC server should be a valid and first-class web server in addition to supporting an easy SDK.
There are two possible solutions:
- Use the underlying FastAPI methods to define routes. That is,
@app.get('/...')
is still valid for a FastRPCapp
. - Use a "sibling" for routes that should have the same function names. That is,
@app.getRPC(sibling=getRecords)
will create two routes that begin with/getrecords
, regardless of the name of the decorated function. Note that if the two functions have otherwise identical signatures, they will define the same underlying route and FastRPC will either allow or prevent this router collision.
These both fully solve the second issue (the web API can have a desired consistency), but the Javascript client function names may still be strange. Thankfully, a good IDE will autocomplete functions beginning with the same name and their Python docstrings will disambiguate them in the Javascript client.
Currently, there is only a @app.getRPC
decorator, but @app.postRPC
, and generic @app.RPC('GET')
decorators are planned.
app = FastRPC(prefix='/api')
# or
app = FastRPC(prefix='/rpc')
Multiple FastAPI APIRouter
objects can be added to a FastRPC
object in the same way they are added to FastAPI
object.
The resulting pythonClient
object can be used in any Javascript runtime (NodeJS, browser, etc.), since it just uses web requests.
Any custom fetch
function can be substituted into the client (e.g. to route to remote services or over a unix domain socket if on the same machine). The default is the runtime's default fetch
; an overriden default fetch
will be used by the client.