Build and deploy a REST API with Postgres database in TypeScript
Tags: webdev, cloud, typescript, typesafe
Published: 2024-04-15 11:20:44

In this tutorial you will create a REST API for a URL Shortener service using [Encore for TypeScript](https://github.com/encoredev/encore), a new way of building fully type-safe and production-ready distributed systems in TypeScript using declarative infrastructure.

With Encore you can declare infrastructure like databases, Pub/Sub, cron jobs, and caches, directly in your application code. Encore takes care of running your local environment, instruments your app with tracing, and automatically provisions and deploys to your cloud in AWS/GCP.

In a few short minutes, you'll learn how to:

* Create REST APIs with Encore
* Use PostgreSQL databases
* Create and run tests
* Deploy to the cloud


## 💽 Install Encore

Install the Encore CLI to run your local environment:
- **macOS:** `brew install encoredev/tap/encore`
- **Linux:** `curl -L https://encore.dev/install.sh | bash`
- **Windows:** `iwr https://encore.dev/install.ps1 | iex`

## Create a service and endpoint

Create a new application by running `encore app create` and select `Empty app` as the template.

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.ts` that looks like this:

```ts
import { api } from "encore.dev/api";
import { randomBytes } from "node:crypto";

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 };
  },
);
```

This sets up the `POST /url` endpoint.

Let’s see if it works! Start your app by running `encore run`.

You should see this:

```output
Encore development server running!

Your API is running at:     http://127.0.0.1:4000
Development Dashboard URL:  http://localhost:9400/5g288
3:50PM INF registered API endpoint endpoint=shorten path=/url service=url
```

💡 You can now open your local development dashboard at: [http://localhost:9400](http://localhost:9400)

In the dashboard you will find an API explorer, complete API documentation and a Service Catalog, and distributed tracing.

![encore local dev dash](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7cawb0wu23bj6xxc30jv.png)


## 🕹 Try it out

Next, call your endpoint:

```shell
$ curl http://localhost:4000/url -d '{"url": "https://encore.dev"}'
```

You should see this:

```output
{
  "id": "5cJpBVRp",
  "url": "https://encore.dev"
}
```

It works!

You can now check out a trace of your request in the local development dashboard, showing you exactly what happened when you made the request.

It will look something like this example:

![trace](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/entrvvoac3jdesep3tq1.jpeg)


## Save URLs in a database

So the API works, but there's just one problem...

Right now, we’re not actually storing the URL anywhere. That means we can generate shortened IDs but there’s no way to get back to the original URL! We need to store a mapping from the short ID to the complete URL.

Happily, Encore makes it very simple 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`).

Add the following contents to the file:

```sql
CREATE TABLE url (
	id TEXT PRIMARY KEY,
	original_url TEXT NOT NULL
);
```

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
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:

```ts
const db = new SQLDatabase("url", { migrations: "./migrations" });
```

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

```ts
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 };
  },
);
```
**Note:** Before running your application in the next step, make sure you have [Docker](https://www.docker.com) installed and running. It's required to locally run Encore applications with databases.

Next, start the application again with `encore run` and Encore automatically sets up your database.

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

Now let's call the API again:

```shell
$ curl http://localhost:4000/url -d '{"url": "https://encore.dev"}'
```

Finally, let's verify that it was saved in the database by running  `encore db shell url` from the app root directory:

```shell
$ encore db shell url
psql (13.1, server 11.12)
Type "help" for help.

url=# select * from url;
    id    |    original_url
----------+--------------------
 zr6RmZc4 | https://encore.dev
(1 row)
```

That was easy!

## Add endpoint to retrieve URLs
To complete our URL shortener API, let’s add the endpoint to retrieve a URL given its short id.

Add this endpoint to `url/url.ts`:

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

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 };
  },
);
```

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.

Let’s make sure it works by calling it (remember to change the `id` below to the one you found in the last step):

```shell
$ curl http://localhost:4000/url/zr6RmZc4
```

You should now see this:

```json
{
  "id": "zr6RmZc4",
  "url": "https://encore.dev"
}
```

And there you have it! That's how you build REST APIs and use PostgreSQL databases in Encore.

## 🚀 Deploy to the cloud

The final step before you deploy is to commit all changes to the project repo.

Commit the new files to the project's git repo and trigger a deploy to Encore's free development cloud by running:

```shell
$ git add -A .
$ git commit -m 'Initial commit'
$ git push encore
```

Encore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud.

After triggering the deployment, you will see a URL where you can view its progress in Encore's [Cloud Dashboard](https://app.encore.dev). It will look something like: `https://app.encore.dev/$APP_ID/deploys/...`

From there you can also see metrics, traces, and connect your own AWS or GCP account to use for production deployment.

*Now you have a fully fledged backend running in the cloud, well done!*

## 🕹 Try it yourself

- Learn more about building Go apps with Encore using [these Tutorials](https://encore.dev/docs/tutorials).👈
- Find inspiration on what to build with these [Open Source App Templates](https://encore.dev/templates).👈

## Wrapping up
- ⭐️ Support the project by [starring Encore on GitHub](https://github.com/encoredev/encore).
- If you have questions or want to share your work, join the developers hangout in Encore's [community on Discord](https://encore.dev/discord).