Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a25a8da
Add Michael's feedback that my use of pipe was wrong.
samwho Jan 31, 2025
4af077e
Twoslashify tutorial introduction.
samwho Jan 31, 2025
cc97fea
Did a pass on the existing material to try and simplify it.
samwho Jan 31, 2025
04e2103
Split out intro from migrating first endpoint.
samwho Jan 31, 2025
df082a4
Begin writing about migrating an endpoint using a database.
samwho Jan 31, 2025
b66883e
Respond to Max's PR feedback.
samwho Feb 7, 2025
2ea679e
Flesh out the hooking up a database section a bit more.
samwho Feb 7, 2025
2942eda
Respond to some more of Max's comments.
samwho Feb 7, 2025
c80459c
Checkpointing my work for the day.
samwho Feb 7, 2025
7aaf3fc
Convert tutorial to use Postgres.
samwho Feb 14, 2025
efbf928
EOD checkpoint.
samwho Feb 14, 2025
d91694b
Update content/src/content/docs/learn/tutorials/hooking-up-a-database…
samwho Feb 21, 2025
4fe4477
Update content/src/content/docs/learn/tutorials/hooking-up-a-database…
samwho Feb 21, 2025
5605e97
Update content/src/content/docs/learn/tutorials/hooking-up-a-database…
samwho Feb 21, 2025
093d1a1
Update content/src/content/docs/learn/tutorials/hooking-up-a-database…
samwho Feb 21, 2025
4652c0a
Round off the 'hooking up a database' page.
samwho Feb 21, 2025
dfb2fab
Start writing the 'Calling an API' page.
samwho Feb 21, 2025
8b1285e
Minor tweak to display of last example.
samwho Feb 21, 2025
c2cc155
First pass on responding to the recorded video feedback.
samwho Mar 7, 2025
55fa6df
Round off the calling an API section.
samwho Mar 7, 2025
9916749
Tidying up.
samwho Mar 7, 2025
cfaa3f5
One last reference to Express.js -> Express
samwho Mar 7, 2025
b7f881c
Incorporate feedback from Livorno.
samwho Mar 28, 2025
f6da245
Started work on a new section about services and layers.
samwho Mar 28, 2025
25bef5f
tweak error css
IMax153 Mar 31, 2025
d832170
Update content/src/content/docs/learn/tutorials/calling-an-api.mdx
samwho Apr 4, 2025
a30d34c
Respond to Max's PR feedback.
samwho Apr 4, 2025
83b3636
Merge branch 'sams-branch' of github.com:samwho/effect-ts-website int…
samwho Apr 4, 2025
63923e7
Checkpointing for the day.
samwho Apr 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions content/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@radix-ui/react-tooltip": "^1.1.4",
"@sentry/opentelemetry": "^8.50.0",
"@tanstack/react-table": "^8.20.5",
"@types/bun": "^1.2.1",
"@types/express": "^5.0.0",
"@types/json-schema": "^7.0.15",
"@types/node": "^22.9.3",
Expand Down
25 changes: 25 additions & 0 deletions content/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

262 changes: 262 additions & 0 deletions content/src/content/docs/learn/tutorials/first-endpoint.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
---
type: Tutorial
title: Migrating our first endpoint
tags:
- some
- tags
sidebar:
order: 2
---

The first step to migrating this to Effect is to launch the Express.js server
with Effect instead of the traditional `app.listen`. Because the Express.js
`app` is a function that returns a node `http.Server` under the hood, it slots
nicely into Effect's `HttpServer` abstraction.

```ts twoslash {3-6, 14-21} title=index.ts
import express from "express"

import { HttpRouter, HttpServer } from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Layer } from "effect"
import { createServer } from "node:http"

const app = express()

app.get("/health", (req, res) => {
res.type("text/plain").send("ok")
})

NodeRuntime.runMain(
Layer.launch(
Layer.provide(
HttpServer.serve(HttpRouter.empty),
NodeHttpServer.layer(() => createServer(app), { port: 3000 }),
),
),
)
```

And we can run the resulting code like so:

```shell
$ bun add @effect/platform @effect/platform-node
$ bun index.ts
```

We haven't changed anything about how the server behaves. It still exposes a
single endpoint, `http://localhost:3000/health`, and that still returns `ok`.
But we've wrapped the Express.js app in a way that will allow us to define new
endpoints using Effect while still responding to our existing endpoints in
Express.js.

There's a lot of new things happening at the bottom of our file. For the time
being we're going to ignore most of it and focus on migrating an endpoint to
Effect. I promise by the time we're done, you'll understand every line we added
above.

First, let's break out our router, `HttpRouter.empty`, out into its own variable
so it's easier to add to.

```ts twoslash
import { HttpRouter } from "@effect/platform"
// ---cut---
const router = HttpRouter.empty
```

And let's define the function we want to run when we hit the `/health` endpoint:

```ts twoslash
import { HttpServerResponse } from "@effect/platform"
// ---cut---
function health() {
return HttpServerResponse.text("ok")
}
```

`HttpServerResponse` is Effect's class for generating HTTP responses, we need to
use it for any responses returned from our endpoints defined with Effect.

The way we wire these things together is going to look a bit strange, but bear
with me.

```ts twoslash
import { HttpServerResponse, HttpRouter } from "@effect/platform"
import { Effect } from "effect"
// ---cut---
function health() {
return HttpServerResponse.text("ok")
}

const router = HttpRouter.empty.pipe(
HttpRouter.get("/health", Effect.sync(health)),
)
```

What on earth are `Effect.sync` and `.pipe`?

## The Effect Type

At the core of Effect is... well, the `Effect`. You can think of an `Effect` as
a lazy computation, similar to a function that hasn't been called yet.

Here's an example of an `Effect` that returns the string `"Hello, world!"`:

```ts twoslash
import { Effect } from "effect"
// ---cut---
Effect.promise(async () => "Hello, world!")
```

`Effect.promise` takes a function that returns a `Promise` and creates an
`Effect` out of it. `Effect.sync` works exactly the same, except it takes a
synchronous function instead of an asynchronous one.

If we want to run this Effect, we can use `Effect.runPromise`:

```ts twoslash {2}
import { Effect } from "effect"
// ---cut---
const effect = Effect.promise(async () => "Hello, world!")
const result = await Effect.runPromise(effect);
console.log(result)
// => "Hello, world!"
```

What if you want to run an Effect that calls another Effect? You can use
`Effect.gen` to do that:

```ts twoslash {2-5}
import { Effect } from "effect"
// ---cut---
const effect = Effect.promise(async () => "Hello, world!")
const gen = Effect.gen(function* () {
const str = yield* effect
return str.toUpperCase()
})
const result = await Effect.runPromise(gen)
console.log(result)
// => "HELLO, WORLD!"
```

This makes use of JavaScript's [generator functions][1]. You can think of the
`yield*` keyword as being very similar to `await`ing a `Promise`. Under the
hood, all `Effect`s are generators, and `yield*`ing one passes it to the Effect
runtime and waits for the result.

Given that all we want to do above is call `.toUpperCase()` on the result
of `effect`, the scaffolding of `Effect.gen` may feel heavy. Effect gives us
a suite of tools to work with `Effect`s, and one of those tools is `pipe`:

```ts twoslash {2}
import { Effect } from "effect"
// ---cut---
const effect = Effect.promise(async () => "Hello, world!")
const upper = effect.pipe(Effect.map((s) => s.toUpperCase()))
const result = await Effect.runPromise(upper)
console.log(result)
// => "HELLO, WORLD!"
```

`pipe` passes the result of one computation as input to another. Here,
`Effect.map` transforms the result of the first `Effect` with the function
provided.

We can `pipe` as many `Effect`s together as we like:

```ts twoslash {2-7}
import { Effect } from "effect"
// ---cut---
const effect = Effect.promise(async () => "Hello, world!")
const upper = effect.pipe(
Effect.map((s) => s.toUpperCase()),
Effect.map((s) => s.split("")),
Effect.map((s) => s.reverse()),
Effect.map((s) => s.join("")),
)
const result = await Effect.runPromise(upper)
console.log(result)
// => "!DLROW ,OLLEH"
```

This should give us just enough to carry on with migrating our first endpoint
to Effect. As we continue through this tutorial, we'll introduce more and more
things you can do with the `Effect` type.

## Understanding `HttpRouter`

Looking back at our `HttpRouter`, we used `pipe` to add a new endpoint to our
app:

```ts twoslash
import { HttpRouter, HttpServerResponse } from "@effect/platform"
import { Effect } from "effect"
function health() {
return HttpServerResponse.text("ok")
}
// ---cut---
const router = HttpRouter.empty.pipe(
HttpRouter.get("/health", Effect.sync(health)),
);
```

`HttpRouter` is a data structure that represents a collection of routes. The
simplest router is one with no routes at all, and Effect exposes that to us as
the `HttpRouter.empty` value. Under the hood, this is itself an `Effect`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
the `HttpRouter.empty` value. Under the hood, this is itself an `Effect`:
the `HttpRouter.empty` value. Under the hood, an `HttpRouter` is itself an `Effect`:


```ts twoslash
import { HttpRouter } from "@effect/platform"
import { Effect } from "effect"
// ---cut---
console.log(Effect.isEffect(HttpRouter.empty))
// => true
```

The helper `HttpRouter.get` takes an `HttpRouter` as an argument and returns a
new `HttpRouter` with the given route added. If we wanted to, we could have done
this much more directly:

```ts twoslash
import { HttpRouter, HttpServerResponse } from "@effect/platform"
import { Effect } from "effect"
function health() {
return HttpServerResponse.text("ok")
}
// ---cut---
const router = HttpRouter.get("/health", Effect.sync(health))(HttpRouter.empty)
```

This is exactly the same as the `pipe` version, except that if we wanted to add
multiple routes it gets unwieldy quickly:

```ts twoslash
import { HttpRouter, HttpServerResponse } from "@effect/platform"
import { Effect } from "effect"
function health() {
return HttpServerResponse.text("ok")
}
function status() {
return HttpServerResponse.text("ok")
}
function version() {
return HttpServerResponse.text("ok")
}
// ---cut---
HttpRouter.get("/health", Effect.sync(health))(
HttpRouter.get("/status", Effect.sync(status))(
HttpRouter.get("/version", Effect.sync(version))(
HttpRouter.empty,
),
),
)

// vs

HttpRouter.empty.pipe(
HttpRouter.get("/version", Effect.sync(version)),
HttpRouter.get("/status", Effect.sync(status)),
HttpRouter.get("/health", Effect.sync(health)),
)
```

[1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
Loading