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
- Use the local development dashboard to test your app
- Create and run tests
This is the end result:
Project
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
If you haven't already, create a new application by running encore app create and select Empty app as the template.
If this is the first time you're using Encore, you'll be asked if you wish to create a free account. This is needed when you want Encore to manage functionality like secrets and handle cloud deployments (which we'll use later on in the tutorial).
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:
url/url.go// Service url takes URLs, generates random short IDs, and stores the URLs in a database.
package url
import (
"context"
"crypto/rand"
"encoding/base64"
)
type URL struct {
ID string // short-form URL id
URL string // complete URL, in long form
}
type ShortenParams struct {
URL string // the URL to shorten
}
// Shorten shortens a URL.
//encore:api public method=POST path=/url
func Shorten(ctx context.Context, p *ShortenParams) (*URL, error) {
id, err := generateID()
if err != nil {
return nil, err
}
return &URL{ID: id, URL: p.URL}, nil
}
// generateID generates a random short ID.
func generateID() (string, error) {
var data [6]byte // 6 bytes of entropy
if _, err := rand.Read(data[:]); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(data[:]), nil
}
This sets up the POST /url endpoint (see the //encore:api annotation on the Shorten function).
π₯ Letβs see if it works! Start your app by running encore run.
You should see this:
Your API is running at: http://localhost:4000 Development Dashboard URL: http://localhost:9400 4:19PM TRC registered endpoint path=/url service=url endpoint=Shorten
π₯ Next, call your endpoint from the Local Development Dashboard at http://localhost:9400 and view a trace of the response. It should look like this:
You can also call it from the terminal:
$ curl http://localhost:4000/url -d '{"URL": "https://encore.dev"}'
And 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:
url/migrations/1_create_tables.up.sqlCREATE TABLE url (
id TEXT PRIMARY KEY,
original_url TEXT NOT NULL
);
π₯ Next, go back to the url/url.go file and import the encore.dev/storage/sqldb package by modifying the import statement to become:
url/url.goimport (
"context"
"crypto/rand"
"encoding/base64"
"encore.dev/storage/sqldb"
)
π₯ Then let's define our database object by adding the following to url/url.go:
url/url.go// Define a database named 'url', using the database
// migrations in the "./migrations" folder.
var db = sqldb.NewDatabase("url", sqldb.DatabaseConfig{
Migrations: "./migrations",
})
π₯ Now, to insert data into our database, letβs create a helper function insert:
url/url.go// insert inserts a URL into the database.
func insert(ctx context.Context, id, url string) error {
_, err := db.Exec(ctx, `
INSERT INTO url (id, original_url)
VALUES ($1, $2)
`, id, url)
return err
}
π₯ Lastly, we can update our Shorten function to insert into the database:
url/url.gofunc Shorten(ctx context.Context, p *ShortenParams) (*URL, error) {
id, err := generateID()
if err != nil {
return nil, err
} else if err := insert(ctx, id, p.URL); err != nil {
return nil, err
}
return &URL{ID: id, URL: p.URL}, nil
}
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.)
You can verify that the database was created by opening the Infra tab in the local development dashboard at localhost:9400, which should something like this:
π₯ Now let's call the API again from the local development dashboard, or from the terminal:
$ curl http://localhost:4000/url -d '{"URL": "https://encore.dev"}'
π₯ Finally, let's verify that it was saved in the database. You can do this by checking the trace in the local development dashboard, or you can run encore db shell url from the app root directory and inputting select * from url;:
$ encore db shell urlpsql (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.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) {
u := &URL{ID: id}
err := db.QueryRow(ctx, `
SELECT original_url FROM url
WHERE id = $1
`, id).Scan(&u.URL)
return u, err
}
Encore uses the path=/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 int or bool if you want to restrict the values.
π₯ We can make sure it works by reviewing the endpoint in the Service Catalog in the local development dashboard, where we can call it using the id you got in the previous step:
You can also call it directly from the terminal:
$ curl http://localhost:4000/url/zr6RmZc4
You should now see this:
{
"ID": "zr6RmZc4",
"URL": "https://encore.dev"
}
It works! 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.
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.go.
url/url_test.gopackage url
import (
"context"
"testing"
)
// TestShortenAndRetrieve - test that the shortened URL is stored and retrieved from database.
func TestShortenAndRetrieve(t *testing.T) {
testURL := "https://github.com/encoredev/encore"
sp := ShortenParams{URL: testURL}
resp, err := Shorten(context.Background(), &sp)
if err != nil {
t.Fatal(err)
}
wantURL := testURL
if resp.URL != wantURL {
t.Errorf("got %q, want %q", resp.URL, wantURL)
}
firstURL := resp
gotURL, err := Get(context.Background(), firstURL.ID)
if err != nil {
t.Fatal(err)
}
if *gotURL != *firstURL {
t.Errorf("got %v, want %v", *gotURL, *firstURL)
}
}
π₯ 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
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!
We're excited to hear what you're going to build with Encore, join the pioneering developer community on Discord and share your story.