Building a Fast Global Serverless API with Turso and endpts

Turso is an edge-hosted, distributed database based on libSQL, a fork of SQLite. It's designed to minimize query latency for global applications by running read-replicas in several regions around the world.

This makes Turso a great choice for building serverless APIs that need to be fast and globally available. In this article, we'll build a fast, global, serverless API with Turso and endpts.

Creating a Turso Database

Our database will contain a list of the top web frameworks and a few details about each one. We'll use this database to build a simple API that returns the list of frameworks and allows us to create new entries. The schema and sample data we will be using is based on this Turso sample repository.

Let's start off by creating the database named topwebframeworks:

turso db create topwebframeworks --location iad

This will create a primary database named topwebframeworks in Ashburn, Virginia (US) (iad) — the default region for endpts Serverless Functions.

Create a Turso database via the CLI

Now let's connect to our database to create our schema and populate it with some data:

turso db shell topwebframeworks

Once connected, we can add our sample data:

-- create frameworks table
CREATE TABLE frameworks (
  id INTEGER PRIMARY KEY,
  name VARCHAR (50) NOT NULL,
  language VARCHAR (50) NOT NULL,
  url TEXT NOT NULL,
  stars INTEGER NOT NULL
);

-- "name" column unique index
CREATE UNIQUE INDEX idx_frameworks_name ON frameworks (name);

-- "url" column unique index
CREATE UNIQUE INDEX idx_frameworks_url ON frameworks (url);

-- seed some data
INSERT INTO frameworks(name, language, url, stars) VALUES
  ("Vue.js" , "JavaScript", "https://github.com/vuejs/vue", 203000),
  ("React", "JavaScript", "https://github.com/facebook/react", 206000),
  ("Angular", "TypeScript", "https://github.com/angular/angular", 87400),
  ("ASP.NET Core", "C#", "https://github.com/dotnet/aspnetcore", 31400),
  ("Express", "JavaScript", "https://github.com/expressjs/express", 60500),
  ("Django", "Python", "https://github.com/django/django", 69800),
  ("Ruby on Rails", "Ruby", "https://github.com/rails/rails", 52600),
  ("Spring", "Java", "https://github.com/spring-projects/spring-framework", 51400),
  ("Laravel", "PHP", "https://github.com/laravel/laravel", 73100),
  ("Flask", "Python", "https://github.com/pallets/flask", 62500),
  ("Ruby", "Ruby", "https://github.com/ruby/ruby", 41000),
  ("Symfony", "PHP", "https://github.com/symfony/symfony", 28200),
  ("CodeIgniter", "PHP", "https://github.com/bcit-ci/CodeIgniter", 18200),
  ("CakePHP", "PHP", "https://github.com/cakephp/cakephp", 8600),
  ("Qwik", "TypeScript", "https://github.com/BuilderIO/qwik", 16400);

Creating a read replica

We currently have a single primary database in Ashburn, Virginia (US) (iad). Now imagine we have a user in Europe who wants to use our API. Each time an API request is made, it will have to travel all the way to the East Coast of the United States and back, which could add a few hundred milliseconds of latency.

To serve our EU users faster, we can create a read-replica in Europe. This will allow us to serve read requests from the replica, which is much closer to our EU users and our serverless functions that we will be deploying there too.

turso db replicate topwebframeworks fra
Create a database read replica

The read replica will be created in Frankfurt, Germany (fra), containing the same data as the primary database. Now, when a user in Europe makes a request to our API, Turso will automatically route the query to the replica in Frankfurt, which is much closer to the user.

Of course, you can choose to create more read replicas wherever you have the majority of your users.

Building our API endpoints

Now that we have our database set up, let's move on to building our global, serverless API. First, we'll need to create a new endpts project:

npm create endpts@latest turso-db
Create an endpts project locally

and install the @libsql/client package to connect to our Turso database:

cd turso-db
npm install --save @libsql/client

To be able to connect to our database, we'll need to get the URL and token from the Turso CLI:

turso db show topwebframeworks --url
turso db tokens create topwebframeworks -e none

The -e flag specifies the expiration date of the token. In this case, we're setting it to none so that the token never expires.

Copy the URL and token and add them to the .env file in the root of your project:

TURSO_DB_URL=...
TURSO_DB_AUTH_TOKEN=...

Let's fire up the dev server and start adding our routes:

npm run dev

First, we'll need to create a client to connect to our database. We will create it in the lib/ directory in the root of our project so that we can reuse it in multiple endpoints:

// lib/db.ts
import { createClient } from '@libsql/client/web'

export const getClient = () => {
  const url = process.env.TURSO_DB_URL
  const authToken = process.env.TURSO_DB_AUTH_TOKEN

  if (!url || !authToken) {
    throw new Error(
      'Please fill the TURSO_DB_URL and TURSO_DB_AUTH_TOKEN env variables'
    )
  }

  return createClient({
    url,
    authToken,
  })
}

The client will use the TURSO_DB_URL and TURSO_DB_AUTH_TOKEN environment variables to connect to our database.

Now let's create our first endpoint, GET /frameworks, that will query the database and return a JSON list of the top web frameworks:

// routes/get_frameworks.ts
import { getClient } from '../lib/db.js'
import type { Route } from '@endpts/types'

export default {
  method: 'GET',
  path: '/frameworks',
  async handler() {
    const client = getClient()

    const { rows: frameworks } = await client.execute(
      'SELECT * FROM frameworks ORDER BY stars DESC'
    )

    return Response.json(frameworks)
  },
} satisfies Route

We can run a quick test to make sure the endpoint works as expected:

curl http://localhost:3000/frameworks
Test the endpoint locally

Similarly, we can create a POST /frameworks endpoint that will insert a new framework into the database:

// routes/post_framework.ts
import { getClient } from '../lib/db.js'
import type { Route } from '@endpts/types'

export default {
  method: 'POST',
  path: '/frameworks',
  async handler(req) {
    const { name, language, url, stars } = await req.json()

    if (!name || !language || !url || !stars) {
      return Response.json({ message: 'Missing fields!' }, { status: 400 })
    }

    const client = getClient()

    const { rows: frameworks } = await client.execute({
      sql: 'INSERT INTO frameworks(name, language, url, stars) VALUES(?, ?, ?, ?) RETURNING *',
      args: [name, language, url, stars],
    })

    return Response.json(frameworks[0])
  },
} satisfies Route

We can test the endpoint by sending a POST request to http://localhost:3000/frameworks with a JSON body:

curl http://localhost:3000/frameworks \
  -d '{ "name": "endpts", "language": "TypeScript", "url": "https://endpts.io", "stars": 50000 }'

Deploying our API globally

The last step is to deploy our API to the cloud to the regions closest to our database.

Head over to the endpts dashboard and create a new project — you can connect your GitHub account to automatically deploy your code on every push or use the manual deployment option and specify a repository URL (e.g.: https://github.com/endpts-samples/turso-db):

Create an endpts project via the Dashboard

Once you've created the project, you will need to add the database URL and token as environment variables. This can be done under the Settings tab of the project:

Set environment variables via the Dashboard

The final step is to select the Deployment Regions for our API. Generally, it's best to keep the API and database as close as possible to minimize the round trip times. With that, let's select the same regions as our database:

Select deployment regions via the Dashboard

Finally, we can push to the repository and endpts will clone the repository, build it, and deploy it to the regions we've selected in just a couple of minutes:

Project details in the Dashboard

Once the deployment has completed, we can get the deployment URLs and test our API in the cloud:

Deployment details in the Dashboard

Conclusion

In this article, we've seen how to create a global, serverless API that connects to a multi-region Turso database. The endpts platform takes care of deploying your serverless functions around the globe while Turso makes it easy to replicate your database to multiple regions worldwide. This keeps your data close to the serverless functions, giving your users a fast and responsive experience wherever they are.