Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
update tutorials
Browse files Browse the repository at this point in the history
marcuskohlberg committed Jan 28, 2025
1 parent 8bc0b1d commit 31d980d
Showing 6 changed files with 163 additions and 45 deletions.
1 change: 1 addition & 0 deletions docs/go/quick-start.mdx
Original file line number Diff line number Diff line change
@@ -53,6 +53,7 @@ Let's look at the code to better understand how to build applications with Encor
You should see this:

```go
-- hello/hello.go --
// Service hello implements a simple hello world REST API.
package hello

21 changes: 14 additions & 7 deletions docs/go/tutorials/rest-api.mdx
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@ Now let's create a new `url` service.
🥐 In your application's root folder, create a new folder `url` and create a new file `url.go` that looks like this:

```go
-- url/url.go --
// Service url takes URLs, generates random short IDs, and stores the URLs in a database.
package url

@@ -121,6 +122,7 @@ Fortunately, Encore makes it really easy to set up a PostgreSQL database to stor
🥐 Add the following contents to the file:

```sql
-- url/migrations/1_create_tables.up.sql --
CREATE TABLE url (
id TEXT PRIMARY KEY,
original_url TEXT NOT NULL
@@ -130,6 +132,8 @@ CREATE TABLE url (
🥐 Next, go back to the `url/url.go` file and import the `encore.dev/storage/sqldb` package by modifying the import statement to become:

```go
-- url/url.go --
HL url/url.go 5:5
import (
"context"
"crypto/rand"
@@ -142,6 +146,7 @@ import (
🥐 Then let's define our database object by adding the following to `url/url.go`:

```go
-- url/url.go --
// Define a database named 'url', using the database
// migrations in the "./migrations" folder.

@@ -153,6 +158,7 @@ var db = sqldb.NewDatabase("url", sqldb.DatabaseConfig{
🥐 Now, to insert data into our database, let’s create a helper function `insert`:

```go
-- url/url.go --
// insert inserts a URL into the database.
func insert(ctx context.Context, id, url string) error {
_, err := db.Exec(ctx, `
@@ -166,6 +172,7 @@ func insert(ctx context.Context, id, url string) error {
🥐 Lastly, we can update our `Shorten` function to insert into the database:

```go
-- url/url.go --
func Shorten(ctx context.Context, p *ShortenParams) (*URL, error) {
id, err := generateID()
if err != nil {
@@ -187,11 +194,9 @@ Before running your application, make sure you have [Docker](https://www.docker.

(In case your application won't run, check the [databases troubleshooting guide](/docs/develop/databases#troubleshooting).)

You can verify that the database was created by looking at your application's Flow architecture diagram in the local development dashboard at [localhost:9400](http://localhost:9400), which should look like this:
You can verify that the database was created by opening the **Infra** tab in the local development dashboard at [localhost:9400](http://localhost:9400), which should something like this:

<video autoPlay playsInline loop controls muted className="w-full h-full">
<source src="/assets/docs/rest_tut_2.mp4" className="w-full h-full" type="video/mp4"/>
</video>
<img src="/assets/docs/infra_tab.png" alt="Infra tab in local development dashboard" className="w-full h-full" />

🥐 Now let's call the API again from the local development dashboard, or from the terminal:

@@ -221,6 +226,7 @@ To complete our URL shortener API, let’s add the endpoint to retrieve a URL gi
🥐 Add this endpoint to `url/url.go`:

```go
-- url/url.go --
// Get retrieves the original URL for the id.
//encore:api public method=GET path=/url/:id
func Get(ctx context.Context, id string) (*URL, error) {
@@ -265,9 +271,12 @@ the service works properly. Such tests including database access
are easy to write.

We've prepared a test to check that the whole cycle of shortening
the URL, storing and then retrieving the original URL works. It looks like this:
the URL, storing and then retrieving the original URL works.

🥐 Save this in a separate file `url/url_test.go`.

```go
-- url/url_test.go --
package url

import (
@@ -299,8 +308,6 @@ func TestShortenAndRetrieve(t *testing.T) {
}
```

🥐 Save this in a separate file `url/url_test.go`.

🥐 Now run `encore test ./...` to verify that it's working.

If you use the local development dashboard ([localhost:9400](http://localhost:9400)), you can even see traces for tests.
9 changes: 8 additions & 1 deletion docs/go/tutorials/slack-bot.md
Original file line number Diff line number Diff line change
@@ -77,6 +77,7 @@ on [Enabling interactivity with Slash Commands](https://api.slack.com/interactiv
🥐 In your Encore app, create a new directory named `slack` and create a file `slack/slack.go` with the following contents:

```go
-- slack/slack.go --
// Service slack implements a cowsaw Slack bot.
package slack
@@ -149,6 +150,7 @@ Let's define a secret using Encore's secrets management functionality.
🥐 Add this to your `slack.go` file:

```go
-- slack/slack.go --
var secrets struct {
SlackSigningSecret string
}
@@ -166,7 +168,9 @@ You can use the same secret value or a placeholder value.
Go makes computing HMAC very straightforward, but it's still a fair amount of code.

🥐 Add a few more imports to your file, so that it reads:

```go
-- slack/slack.go --
import (
"crypto/hmac"
"crypto/sha256"
@@ -188,6 +192,7 @@ import (
🥐 Next, we'll add the `verifyRequest` function:

```go
-- slack/slack.go --
// verifyRequest verifies that a request is coming from Slack.
func verifyRequest(req *http.Request) (body []byte, err error) {
eb := errs.B().Code(errs.InvalidArgument)
@@ -236,6 +241,7 @@ We're now ready to verify the signature.
🥐 Update the `Cowsay` function to look like this:

```go
-- slack/slack.go --
//encore:api public raw path=/cowsay
func Cowsay(w http.ResponseWriter, req *http.Request) {
body, err := verifyRequest(req)
@@ -259,9 +265,10 @@ func Cowsay(w http.ResponseWriter, req *http.Request) {

Finally we're ready to put it all together.

🥐 Update the `cowart` like so:
🥐 Add the `cowart` like so:

```go
-- slack/slack.go --
const cowart = `
________________________________________
< %- 38s >
2 changes: 2 additions & 0 deletions docs/ts/quick-start.mdx
Original file line number Diff line number Diff line change
@@ -52,6 +52,7 @@ Let's look at the code to better understand how to build applications with Encor
You should see this:

```ts
-- hello/hello.ts --
import { api } from "encore.dev/api";

export const world = api(
@@ -73,6 +74,7 @@ You define an API endpoint by wrapping a regular async function in a call to `ap
The `world` endpoint is part of the `hello` service because in the same folder you will also find a file named `encore.service.ts` which looks like this:

```ts
-- hello/encore.service.ts --
import { Service } from "encore.dev/service";

export default new Service("hello");
164 changes: 128 additions & 36 deletions docs/ts/tutorials/rest-api.mdx
Original file line number Diff line number Diff line change
@@ -53,7 +53,13 @@ export default new Service("url");
This is how you define a service with Encore. Encore will now consider files in the `url` directory and all its subdirectories as part of the `url` service.


🥐 Create a new file `url.ts` in the `url` directory that looks like this:
🥐 Create a new file `url.ts` in the `url` directory:

```shell
$ touch url/url.ts
```

🥐 Add the following code to `url/url.ts`:

```ts
-- url/url.ts --
@@ -81,7 +87,11 @@ export const shorten = api(

This sets up the `POST /url` endpoint.

🥐 Let’s see if it works! Start your app by running `encore run`.
🥐 Let’s see if it works! Start your app by running the following command from your app's root directory:

```shell
$ encore run
```

You should see this:

@@ -122,11 +132,17 @@ Right now, we’re not actually storing the URL anywhere. That means we can gene
## 2. Save URLs in a database
Fortunately, Encore makes it really easy to set up a PostgreSQL database to store our data. To do so, we first define a **database schema**, in the form of a migration file.

🥐 Create a new folder named `migrations` inside the `url` folder. Then, inside the `migrations` folder, create an initial database migration file named `1_create_tables.up.sql`. The file name format is important (it must start with `1_` and end in `.up.sql`).
🥐 Create a new folder named `migrations` inside the `url` folder. Then, inside the `migrations` folder, create an initial database migration file named `001_create_tables.up.sql`. The file name format is important (it must start with `001_` and end in `.up.sql`).

```shell
$ mkdir url/migrations
$ touch url/migrations/001_create_tables.up.sql
```

🥐 Add the following contents to the file:

```sql
-- url/migrations/001_create_tables.up.sql --
CREATE TABLE url (
id TEXT PRIMARY KEY,
original_url TEXT NOT NULL
@@ -136,31 +152,77 @@ CREATE TABLE url (
🥐 Next, go back to the `url/url.ts` file and import the `SQLDatabase` class from `encore.dev/storage/sqldb` module by modifying the imports to look like this:

```ts
-- url/url.ts --
HL url/url.ts 1:1
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { randomBytes } from "node:crypto";
```

🥐 Now, to insert data into our database, let’s create an instance of the `SQLDatabase` class:
🥐 Now, to define the database, create an instance of the `SQLDatabase` class in the `url` service:

```ts
-- url/url.ts --
HL url/url.ts 4:5
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { randomBytes } from "node:crypto";

// 'url' database is used to store the URLs that are being shortened.
const db = new SQLDatabase("url", { migrations: "./migrations" });

interface URL {
id: string; // short-form URL id
url: string; // complete URL, in long form
}

interface ShortenParams {
url: string; // the URL to shorten
}

// Shortens a URL.
export const shorten = api(
{ method: "POST", path: "/url", expose: true },
async ({ url }: ShortenParams): Promise<URL> => {
const id = randomBytes(6).toString("base64url");
return { id, url };
},
);
```

🥐 Lastly, we can update our `shorten` function to insert into the database:
🥐 Lastly, update the `shorten` function to insert data into the database:

```ts
-- url/url.ts --
HL url/url.ts 21:24
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { randomBytes } from "node:crypto";

// 'url' database is used to store the URLs that are being shortened.
const db = new SQLDatabase("url", { migrations: "./migrations" });

interface URL {
id: string; // short-form URL id
url: string; // complete URL, in long form
}

interface ShortenParams {
url: string; // the URL to shorten
}

// Shortens a URL.
export const shorten = api(
{ method: "POST", path: "/url", expose: true },
async ({ url }: ShortenParams): Promise<URL> => {
const id = randomBytes(6).toString("base64url");
await db.exec`
INSERT INTO url (id, original_url)
VALUES (${id}, ${url})
`;
return { id, url };
},
);
{ method: "POST", path: "/url", expose: true },
async ({ url }: ShortenParams): Promise<URL> => {
const id = randomBytes(6).toString("base64url");
await db.exec`
INSERT INTO url (id, original_url)
VALUES (${id}, ${url})
`;
return { id, url };
},
);
```

<Callout type="info">
@@ -173,11 +235,9 @@ Before running your application, make sure you have [Docker](https://www.docker.

(In case your application won't run, check the [databases troubleshooting guide](/docs/ts/primitives/databases#troubleshooting).)

You can verify that the database was created by looking at your application's Flow architecture diagram in the local development dashboard at [localhost:9400](http://localhost:9400), which should look like this:
You can verify that the database was created by looking at the **Infra** tab in the local development dashboard at [localhost:9400](http://localhost:9400), which should look like this:

<video autoPlay playsInline loop controls muted className="w-full h-full">
<source src="/assets/docs/rest_tut_2.mp4" className="w-full h-full" type="video/mp4"/>
</video>
<img src="/assets/docs/infra_tab.png" alt="Infra tab in local development dashboard" className="w-full h-full" />

🥐 Now let's call the API again from the local development dashboard, or from the terminal:

@@ -207,18 +267,49 @@ To complete our URL shortener API, let’s add the endpoint to retrieve a URL gi
🥐 Add this endpoint to `url/url.ts`:

```ts
import { APIError } from "encore.dev/api";
-- url/url.ts --
HL url/url.ts 0:0
HL url/url.ts 29:39
import { api, APIError } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { randomBytes } from "node:crypto";

// 'url' database is used to store the URLs that are being shortened.
const db = new SQLDatabase("url", { migrations: "./migrations" });

interface URL {
id: string; // short-form URL id
url: string; // complete URL, in long form
}

interface ShortenParams {
url: string; // the URL to shorten
}

// Shortens a URL.
export const shorten = api(
{ method: "POST", path: "/url", expose: true },
async ({ url }: ShortenParams): Promise<URL> => {
const id = randomBytes(6).toString("base64url");
await db.exec`
INSERT INTO url (id, original_url)
VALUES (${id}, ${url})
`;
return { id, url };
},
);

// Get retrieves the original URL for the id.
export const get = api(
{ method: "GET", path: "/url/:id", expose: true },
async ({ id }: { id: string }): Promise<URL> => {
const row = await db.queryRow`
SELECT original_url FROM url WHERE id = ${id}
`;
if (!row) throw APIError.notFound("url not found");
return { id, url: row.original_url };
},
);
{ expose: true, auth: false, method: "GET", path: "/url/:id" },
async ({ id }: { id: string }): Promise<URL> => {
const row = await db.queryRow`
SELECT original_url FROM url WHERE id = ${id}
`;
if (!row) throw APIError.notFound("url not found");
return { id, url: row.original_url };
}
);
```

Encore uses the `/url/:id` syntax to represent a path with a parameter. The `id` name corresponds to the parameter name in the function signature. In this case it is of type `string`, but you can also use other built-in types like `number` or `boolean` if you want to restrict the values.
@@ -232,14 +323,14 @@ Encore uses the `/url/:id` syntax to represent a path with a parameter. The `id`
You can also call it directly from the terminal:

```shell
$ curl http://localhost:4000/url/zr6RmZc4
$ curl http://localhost:4000/url/your-id-from-the-previous-step
```

You should now see this:

```json
{
"id": "zr6RmZc4",
"id": "your-id-from-the-previous-step",
"url": "https://encore.dev"
}
```
@@ -261,16 +352,19 @@ $ npm i --save-dev vitest
🥐 Next we need to add a test script to our `package.json`:

```json
...
-- package.json --
HL package.json 1:1
"scripts": {
"test": "vitest"
},
...
```

We've prepared a test to check that the whole cycle of shortening the URL, storing and then retrieving the original URL works. It looks like this:
We've prepared a test to check that the whole cycle of shortening the URL, storing and then retrieving the original URL works.

🥐 Save this in a separate file `url/url.test.ts`.

```ts
-- url/url.test.ts --
import { describe, expect, test } from "vitest";
import { get, shorten } from "./url";

@@ -283,8 +377,6 @@ describe("shorten", () => {
});
```

🥐 Save this in a separate file `url/url.test.ts`.

🥐 Now run `encore test` to verify that it's working.

If you use the local development dashboard ([localhost:9400](http://localhost:9400)), you can even see traces for tests.
11 changes: 10 additions & 1 deletion docs/ts/tutorials/slack-bot.md
Original file line number Diff line number Diff line change
@@ -95,6 +95,7 @@ This is how you create define services with Encore. Encore will now consider fil
🥐 Create a file `slack/slack.ts` with the following contents:

```ts
-- slack/slack.ts --
import { api } from "encore.dev/api";
import type { IncomingMessage } from "node:http";
@@ -176,6 +177,9 @@ Let's define a secret using Encore's secrets management functionality.
🥐 Add this to your `slack.ts` file:

```ts
-- slack/slack.ts --
HL slack/slack.ts 0:0
HL slack/slack.ts 2:2
import { secret } from "encore.dev/config";
const slackSigningSecret = secret("SlackSigningSecret");
@@ -194,13 +198,15 @@ TypeScript makes computing HMAC very straightforward, but it's still a fair amou

🥐 Add a few more imports to your file, so that it reads:
```ts
-- slack/slack.ts --
import { createHmac, timingSafeEqual } from "node:crypto";
import type { IncomingHttpHeaders } from "http";
```

🥐 Next, we'll add the `verifySignature` function:

```ts
-- slack/slack.ts --
// Verifies the signature of an incoming request from Slack.
const verifySignature = async function (
body: string,
@@ -259,6 +265,8 @@ We're now ready to verify the signature.
🥐 Update the `cowsay` function to look like this:

```ts
-- slack/slack.ts --
HL slack/slack.ts 5:12
export const cowsay = api.raw(
{ expose: true, path: "/cowsay", method: "*" },
async (req, resp) => {
@@ -285,9 +293,10 @@ export const cowsay = api.raw(

Finally we're ready to put it all together.

🥐 Update the `cowart` like so:
🥐 Add the `cowart` in `slack.ts` like so:

```ts
-- slack/slack.ts --
const cowart = (msg: string) => `
\`\`\`
+-${"-".repeat(msg.length)}-+

0 comments on commit 31d980d

Please sign in to comment.