Building a REST API

Learn how to build a URL shortener with a REST API and PostgreSQL database

In this tutorial you will create a REST API for a URL Shortener service. In a few short minutes, you'll learn how to:

  • Create REST APIs with Encore
  • Use PostgreSQL databases
  • Create and run tests

This is the end result:

Project

Please note

To make it easier to follow along, we've laid out a trail of croissants to guide your way. Whenever you see a 🥐 it means there's something for you to do.

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

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

🥐 Next, call your endpoint:

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

You should see this:

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

It works! 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.

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

🥐 Add the following contents to the file:

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:

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:

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

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

Before running your application, 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"}'

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

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

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

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

You should now see this:

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

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

4. Add a test

Before deployment, it is good practice to have tests to assure that the service works properly. Such tests including database access are easy to write.

🥐 Let's start by adding the vitest package to your project:

$ npm i --save-dev vitest

Vitest is a testing framework that works great with Encore but you can use another TypeScript testing framework if you like.

🥐 Next we need to add a test script to our package.json:

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

import { describe, expect, test } from "vitest"; import { get, shorten } from "./url"; describe("shorten", () => { test("getting a shortened url should give back the original", async () => { const resp = await shorten({ url: "https://example.com" }); const url = await get({ id: resp.id }); expect(url.url).toBe("https://example.com"); }); });

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

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

If you use the local development dashboard (localhost:9400), you can even see traces for tests.

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

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!

Celebrate with fireworks

Now that your app is running in the cloud, let's celebrate with some fireworks:

🥐 In the Cloud Dashboard, open the Command Menu by pressing Cmd + K (Mac) or Ctrl + K (Windows/Linux).

From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints.

🥐 Type fireworks in the Command Menu and press enter. Sit back and enjoy the show!

Fireworks

What's next

Now that you know how to build a backend with a database, you're ready to let your creativity flow and begin building your next great idea!

🥐 A great next step is to integrate with GitHub. Once you've linked with GitHub, Encore will automatically start building and running tests against your Pull Requests.

We're excited to hear what you're going to build with Encore, join the pioneering developer community on Discord and share your story.