> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Local Development

> Edge Compute functions are ordinary programs in containers — run and test them locally with each language's native tooling; a standalone function needs no emulator.

An Edge Compute function is an ordinary program in a container, not code inside a proprietary runtime. The TypeScript and JavaScript scaffolds run their own HTTP server on `$PORT` (default 8080); the Python, Go, and Quarkus scaffolds export a handler and the server is run for you. Local development is therefore unremarkable: run the program, `curl localhost:8080`, iterate, then `telnyx-edge ship`.

Everything below runs against the projects generated by `telnyx-edge new-func` (see the [Quickstart](/docs/edge-compute/quickstart)); the Python and Go *serve* rows add a small local entry point shown in their tabs:

| Language       | Serve locally                                                  | Test            |
| -------------- | -------------------------------------------------------------- | --------------- |
| TypeScript     | `npm run build && npm start`                                   | `node --test`   |
| JavaScript     | `node index.js`                                                | `node --test`   |
| Python         | `uvicorn app:app --port 8080 --interface asgi3 --lifespan off` | `pytest`        |
| Go             | `go run ./cmd/local` (scratch `main`)                          | `go test ./...` |
| Java (Quarkus) | `./mvnw quarkus:dev`                                           | `./mvnw test`   |

## Run and test, by language

<Tabs>
  <Tab title="TypeScript / JavaScript">
    The scaffold's `index.ts` (or `index.js`) at the project root is a `node:http` server. Build (TypeScript only) and run it:

    ```bash theme={null}
    npm install
    npm run build   # tsc → dist/index.js
    npm start       # node dist/index.js — for JavaScript: node index.js
    # → Server running on port 8080
    ```

    `npm run dev` runs `index.ts` directly through `ts-node` with no build step. The server reads `process.env.PORT` and falls back to 8080.

    Exercise it with curl:

    ```bash theme={null}
    curl localhost:8080/
    # → {"message":"Hello from Telnyx Edge Compute!"}

    curl -X POST localhost:8080/ -H "Content-Type: application/json" -d '{"name":"test"}'
    # → {"message":"Hello from Telnyx Edge Compute!","data":{"name":"test"}}

    curl -s -o /dev/null -w "%{http_code}\n" localhost:8080/health
    # → 200
    ```

    The `/health` fast-path at the top of the scaffold answers the platform's probes — keep it fast and dependency-free when you rework the server.

    The scaffold starts its server at module load and exports nothing, so there is nothing to import in a unit test. Keep request handling in exported functions the server delegates to, test those with `node --test`, and treat curl against the running server as your integration test.
  </Tab>

  <Tab title="Python">
    The scaffold is an ASGI application: `function/func.py` exposes a module-level `new()` factory, and the returned instance's `handle(scope, receive, send)` method is the ASGI callable. Any ASGI server runs it — with uvicorn, add a local entry point:

    ```python theme={null}
    # app.py — local entry point, not part of the function contract
    from function import new

    app = new().handle
    ```

    ```bash theme={null}
    pip install uvicorn
    uvicorn app:app --port 8080 --interface asgi3 --lifespan off
    # → Uvicorn running on http://127.0.0.1:8080
    ```

    `--interface asgi3` is required — `handle` is a bound method and uvicorn's interface auto-detection misclassifies it, returning 500s without the flag. `--lifespan off` silences a harmless `Invalid ASGI scope type: lifespan` error at startup — the scaffold doesn't implement the lifespan protocol uvicorn probes for by default. uvicorn also never calls the optional `start(cfg)`/`stop()` hooks; only the platform does.

    For tests, the scaffold's `pyproject.toml` already declares `pytest`, `pytest-asyncio`, and `httpx` as dev dependencies, with `asyncio_mode = "strict"` — mark async tests explicitly. No server is needed: call `handle` directly with a hand-built scope.

    ```bash theme={null}
    pip install -e ".[dev]"
    ```

    ```python theme={null}
    # tests/test_handle.py
    import json

    import pytest

    from function import new


    @pytest.mark.asyncio
    async def test_get_returns_greeting():
        func = new()
        sent = []

        async def receive():
            return {"type": "http.request", "body": b""}

        async def send(message):
            sent.append(message)

        scope = {"type": "http", "method": "GET", "path": "/", "headers": []}
        await func.handle(scope, receive, send)

        assert sent[0]["type"] == "http.response.start"
        assert sent[0]["status"] == 200
        assert json.loads(sent[1]["body"])["message"] == "Hello from Telnyx Edge Compute!"
    ```

    ```bash theme={null}
    pytest
    # → 1 passed
    ```
  </Tab>

  <Tab title="Go">
    The scaffold exports `Handle(w http.ResponseWriter, r *http.Request)` from `package function` — there is no `main()`; the platform provides the server. `net/http/httptest` drives `Handle` without one:

    ```go theme={null}
    // handler_test.go
    package function

    import (
    	"net/http"
    	"net/http/httptest"
    	"strings"
    	"testing"
    )

    func TestHandlePostEchoesJSON(t *testing.T) {
    	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"name":"test"}`))
    	rec := httptest.NewRecorder()

    	Handle(rec, req)

    	if rec.Code != http.StatusOK {
    		t.Fatalf("expected 200, got %d", rec.Code)
    	}
    	if !strings.Contains(rec.Body.String(), `"name":"test"`) {
    		t.Fatalf("body did not echo request: %s", rec.Body.String())
    	}
    }
    ```

    ```bash theme={null}
    go test ./...
    # → ok  	function	0.6s
    ```

    For a curl-able local server, add a scratch `main` in its own package. The import path is `function` because that is the module path in the scaffold's `go.mod`:

    ```go theme={null}
    // cmd/local/main.go — local harness only
    package main

    import (
    	"log"
    	"net/http"

    	"function"
    )

    func main() {
    	http.HandleFunc("/", function.Handle)
    	log.Println("listening on :8080")
    	log.Fatal(http.ListenAndServe(":8080", nil))
    }
    ```

    ```bash theme={null}
    go run ./cmd/local
    curl localhost:8080/
    # → {"message":"Hello from Telnyx Edge Compute!"}
    ```
  </Tab>

  <Tab title="Java (Quarkus)">
    The scaffold is a Quarkus Funqy project. Dev mode gives hot reload on source changes and a debugger on port 5005:

    ```bash theme={null}
    ./mvnw quarkus:dev
    ```

    ```bash theme={null}
    curl localhost:8080/ -H "Content-Type: application/json" -d '{"message":"ping"}'
    # → {"message":"ping"}
    ```

    The test stack (`quarkus-junit5`, `rest-assured`) is already in the `pom.xml`:

    ```bash theme={null}
    ./mvnw test
    ```
  </Tab>
</Tabs>

## Testing

A standalone function needs no emulator: the platform runs your program in a container and sends it HTTP, which is exactly what your terminal just did — the language-standard tools above are the whole testing story. (Projects that add [Stateful Actors](/docs/edge-compute/stateful-actors) run locally with `telnyx-edge dev`; see its docs.)

The one structural habit worth adopting: keep transport separate from logic. Parse, validate, and compute in plain functions; let the server (or `Handle`, or `handle`) stay a thin adapter. That keeps unit tests fast and keeps the binding limitation below out of most of your test suite.

## Framework servers

For TypeScript and JavaScript, the platform contract is a process that binds the port in `$PORT` (default 8080). Any HTTP server that does so runs in the container — Express, Fastify, Hono, or anything else — and the server you ran locally is the server that runs deployed. There are no framework-specific guides.

## Bindings and secrets

The [`env` binding surface](/docs/edge-compute/runtime/bindings) — `[telnyx]` clients, `env.SECRETS.get()`, KV namespaces, actors — resolves only inside a deployed function. A standalone function has no local binding emulation — to exercise binding-backed code paths, ship to a scratch function and curl its live URL. ([Stateful Actors](/docs/edge-compute/stateful-actors) projects are the exception: `telnyx-edge dev` runs their actor stack locally so `env.<BINDING>` calls resolve.)

Plain environment variables are the exception. In production, secrets created with `telnyx-edge secrets add <key> <value>` are injected as environment variables into your functions, and declaring a `[telnyx]` binding injects `TELNYX_API_KEY`. Code that reads plain env vars therefore works locally by exporting the same names:

```bash theme={null}
# Locally: your shell
export MY_API_KEY=test-value

# Deployed: injected as an environment variable
telnyx-edge secrets add MY_API_KEY <real-value>
```

Use throwaway values locally and never commit real ones. See [Secrets](/docs/edge-compute/configuration/secrets) and [Environment variables](/docs/edge-compute/configuration/environment-variables).

## Deploy

When it works locally, ship it:

```bash theme={null}
telnyx-edge ship
# → 📡 Your function is live at:
#      https://<func-name>-<org>.telnyxcompute.com
```

Each successful ship creates an immutable revision; `telnyx-edge revisions list <function>` shows them and `telnyx-edge rollback <function> <revision-id>` retargets traffic to an earlier one. See [Deploy](/docs/edge-compute/deploy).

## Next Steps

* [Deploy](/docs/edge-compute/deploy) — ship, revisions, and rollback
* [HTTP handler](/docs/edge-compute/runtime/http-handler) — the exact entrypoint contract per language
* [Bindings](/docs/edge-compute/runtime/bindings) — the `env` surface a deployed function gets
* [CLI reference](/docs/edge-compute/reference/cli) — every `telnyx-edge` command
