Skip to content

Min 01-02 — The Beginning ⚡

The clock starts now. Two minutes to a running server.

Install

$ pip install heaven

---> 100%
Successfully installed heaven

Write it

Create a file named app.py:

from heaven import App

app = App()

# Every handler in Heaven takes exactly three arguments.
async def hello(req, res, ctx):
    res.body = 'Hello from Heaven'

# Map GET / to the hello handler.
app.GET('/', hello)

Two things to notice, because they hold for the entire framework:

  1. Registration is a method call, not a decorator. app.GET(route, handler).
  2. The handler signature never changes. (req, res, ctx) — for routes, hooks, and websockets alike.

Handlers can be sync too

async def is the right default, but a plain def hello(req, res, ctx) works identically. Heaven checks and calls it correctly. Use sync handlers freely for CPU-only work with no await in it.

Fly it

$ heaven fly

INFO:     Started server process [1234]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Open http://localhost:8000. That's a running server, and you've spent two minutes.

Returning JSON

Assign a dict or a list and Heaven serializes it with orjson and sets Content-Type: application/json for you.

async def hello(req, res, ctx):
    res.body = {'message': 'Hello from Heaven'}   # -> {"message":"Hello from Heaven"}

res.body =, not return

Heaven ignores whatever your handler returns. You communicate by writing to res. This trips up everyone arriving from FastAPI exactly once, and then never again.

async def wrong(req, res, ctx):
    return {'message': 'this is discarded'}    # ❌ returns are ignored

async def right(req, res, ctx):
    res.body = {'message': 'this is sent'}     # ✅

Growing past one file

As soon as you have more than a handful of routes, stop importing handlers and pass their import path as a string instead.

# handlers/users.py
async def get_profile(req, res, ctx):
    res.body = {'id': req.params.get('id')}
# app.py
app.GET('/profile/:id:int', 'handlers.users.get_profile')

Heaven resolves the string at registration time. No import block, no circular imports, no decorator scanning.


Next: You're flying. Now take the controls → Min 03-04 — The Command Line