Back to Blog
Tutorial8 min read2026-07-17

Deploy an Effect (TypeScript) API to Production in 2026

Effect is the TypeScript library that finally makes typed errors, dependency injection, and structured concurrency ergonomic. Here's how to package an Effect HTTP API into a container and ship it on PandaStack.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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 locally

Dockerfile

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

  1. 1Push to Git.
  2. 2https://dashboard.pandastack.io → New App → connect the repo. It detects the Dockerfile and builds it.
  3. 3Need Postgres? Databases → New Database → PostgreSQL, then attach it to the app — DATABASE_URL is injected. Read it inside an Effect Layer so your config is typed and testable:
import { Config, Effect } from "effect"
const dbUrl = Config.string("DATABASE_URL")
  1. 1Push again and it redeploys automatically.

CLI path:

npm install -g @pandastack/cli
panda login
panda deploy

Why 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.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also