If you've written enough TypeScript to be tired of try/catch swallowing errors, untyped throw, and dependency wiring by hand, you've probably eyed Effect (https://effect.website). It brings typed errors, a real dependency-injection layer, retries, interruption, and structured concurrency into TypeScript without leaving TypeScript. What Effect *doesn't* do is deploy itself — an Effect program is still just Node at runtime. I run PandaStack; here's the deployment half, kept practical.
A minimal Effect HTTP API
Effect ships an HTTP platform package. A small server with a typed route and a typed error:
// src/main.ts
import { HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApi } from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Effect, Layer, Schema } from "effect"
import { createServer } from "node:http"
class NotFound extends Schema.TaggedError<NotFound>()("NotFound", {
id: Schema.String,
}) {}
const api = HttpApi.make("api").add(
HttpApiGroup.make("users").add(
HttpApiEndpoint.get("getUser")`/users/${Schema.String}`
.addSuccess(Schema.Struct({ id: Schema.String, name: Schema.String }))
.addError(NotFound),
),
)
const UsersLive = HttpApiBuilder.group(api, "users", (handlers) =>
handlers.handle("getUser", ({ path }) =>
path === "42"
? Effect.succeed({ id: "42", name: "Ada" })
: Effect.fail(new NotFound({ id: path })),
),
)
const ApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(UsersLive))
const Server = HttpApiBuilder.serve().pipe(
Layer.provide(ApiLive),
Layer.provide(NodeHttpServer.layer(createServer, { port: Number(process.env.PORT) || 8080 })),
)
NodeRuntime.runMain(Layer.launch(Server))The important part isn't the exact API surface (Effect evolves fast — check the current docs). It's that your errors (NotFound) are values in the type system, your server is a Layer, and process.env.PORT still drives the listen port. That last detail is all a host needs.
Build to plain JS
Effect apps are TypeScript. Compile them ahead of time so the container doesn't ship a compiler:
{
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/main.js"
}
}npm run build && npm start # confirm it serves locallyDockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 8080
CMD ["node", "dist/main.js"]Deploy on PandaStack
- 1Push to Git.
- 2https://dashboard.pandastack.io → New App → connect the repo. It detects the Dockerfile and builds it.
- 3Need Postgres? Databases → New Database → PostgreSQL, then attach it to the app —
DATABASE_URLis injected. Read it inside an EffectLayerso your config is typed and testable:
import { Config, Effect } from "effect"
const dbUrl = Config.string("DATABASE_URL")- 1Push again and it redeploys automatically.
CLI path:
npm install -g @pandastack/cli
panda login
panda deployWhy Effect + a boring host is a good combo
Effect's whole pitch is that failure is modeled, not hoped away — retries with backoff, timeouts, interruption, and dependency graphs are first-class. That's exactly the code you want running as a long-lived container with health checks and auto-restart, which is what container hosting gives you. Add a /health endpoint (an Effect that returns { ok: true }) so the platform's health checks can restart a wedged instance.
Honest tradeoffs
- Effect has a learning curve. The payoff is real for teams tired of untyped errors, but it's not a free lunch — budget ramp-up time. That's an Effect decision, not a hosting one.
- The API surface moves. Effect iterates quickly; pin your versions and check the docs when you upgrade.
- Free-tier apps scale to zero and cold-start. For an internal API that's usually fine; keep it warm on a paid tier if first-request latency matters.
- PandaStack is a newer platform than the incumbents — great DX, smaller ecosystem, worth knowing.
Wrap-up
Effect gives you typed errors and structured concurrency; PandaStack gives it a container, a managed Postgres, an injected DATABASE_URL, and Git-push deploys. Compile to JS, containerize, ship.
Docs: https://docs.pandastack.io. Start free: https://dashboard.pandastack.io.