Build and deploy a REST API with Postgres database in TypeScript

marcuskohlberg

Marcus Kohlberg

Posted on April 15, 2024

Build and deploy a REST API with Postgres database in TypeScript

In this tutorial you will create a REST API for a URL Shortener service using Encore for TypeScript, 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:



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


Enter fullscreen mode Exit fullscreen mode

This sets up the POST /url endpoint.

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

You should see this:



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


Enter fullscreen mode Exit fullscreen mode

💡 You can now open your local development dashboard at: 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

🕹 Try it out

Next, call your endpoint:



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


Enter fullscreen mode Exit fullscreen mode

You should see this:



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


Enter fullscreen mode Exit fullscreen mode

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

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:



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


Enter fullscreen mode Exit fullscreen mode

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:



import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { randomBytes } from "node:crypto";


Enter fullscreen mode Exit fullscreen mode

Now, to insert data into our database, let’s create an instance of the SQLDatabase class:



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


Enter fullscreen mode Exit fullscreen mode

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



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


Enter fullscreen mode Exit fullscreen mode

Note: Before running your application in the next step, make sure you have Docker 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.)

Now let's call the API again:



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


Enter fullscreen mode Exit fullscreen mode

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



$ 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)


Enter fullscreen mode Exit fullscreen mode

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:



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


Enter fullscreen mode Exit fullscreen mode

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



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


Enter fullscreen mode Exit fullscreen mode

You should now see this:



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


Enter fullscreen mode Exit fullscreen mode

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:



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


Enter fullscreen mode Exit fullscreen mode

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

Wrapping up

💖 💪 🙅 🚩
marcuskohlberg
Marcus Kohlberg

Posted on April 15, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related