DEV Community

Cover image for Create an API with Dart + Heroku
Aswin Gopinathan
Aswin Gopinathan

Posted on

Create an API with Dart + Heroku

When you first started learning Flutter, you might have come across videos on Working with APIs in Dart/Flutter right?
But did you ever think about How to build your own API with Dart ?

API be like:

doggy-gif

Worry not when i am here!

In this tutorial you are gonna learn just two things :

  1. Create your own API with Dart
  2. Host it and make it public using Heroku

But, we need some data which we can send through the API right? 🤔

Let's use Supabase !

Supabase logo

So, i have created a Database table in Supabase with the following content :

Supabase DB image

Check out the official documentation to know how you can create and add rows to your database.

Now head over to your IDE and create a new Dart project and select Dart->Web Server. This article uses IntelliJ, but you can replicate the same steps in your IDE too!

IntelliJ New Project Screen

Enter an appropriate name and create the project.

A bin/server.dart file will be created which contains our server code that will create our API.


Optional Section

This section is for those people who are using Visual Studio, chances are you may not get an option to create a Dart Server App with the above template.

No worries, you can follow the following steps :

1) Create a plain Dart Command-line App :

dart create my_server
Enter fullscreen mode Exit fullscreen mode

2) Rename the file inside bin folder to server.dart. You can use any name, i have used this name so that we are on the same page :P

3) Add the following dependencies in pubspec.yaml file :

dependencies:
  args: ^2.0.0
  shelf: ^1.1.0
Enter fullscreen mode Exit fullscreen mode

4) Add the code given in the section below to your newly created server.dart file.


Your server.dart file should look like this :

import 'dart:io';

import 'package:args/args.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as io;

// For Google Cloud Run, set _hostname to '0.0.0.0'.
const _hostname = 'localhost';

void main(List<String> args) async {
  var parser = ArgParser()..addOption('port', abbr: 'p');
  var result = parser.parse(args);

  // For Google Cloud Run, we respect the PORT environment variable
  var portStr = result['port'] ?? Platform.environment['PORT'] ?? '8080';
  var port = int.tryParse(portStr);

  if (port == null) {
    stdout.writeln('Could not parse port value "$portStr" into a number.');
    // 64: command line usage error
    exitCode = 64;
    return;
  }

  var handler = const shelf.Pipeline()
      .addMiddleware(shelf.logRequests())
      .addHandler(_echoRequest);

  var server = await io.serve(handler, _hostname, port);
  print('Serving at http://${server.address.host}:${server.port}');
}

shelf.Response _echoRequest(shelf.Request request) =>
    shelf.Response.ok('Request for "${request.url}"');

Enter fullscreen mode Exit fullscreen mode

I know you will be like :

Mrs. Geller saying "That's a lot of information in 30 seconds"

So, Let's break it down and understand each line.

1) Defining the Host Name :

const _hostname = 'localhost';
Enter fullscreen mode Exit fullscreen mode

If you are debugging, then we use localhost for testing the application. But we have to change that once we plan to host it somewhere like Heroku or Cloud Run.

We will be changing that at the end of this article where we will be hosting this API on Heroku.

2) Line 11 and Line 12 is for Argument Parser, if you wish to send the port number to use via Command Line Arguments.
eg:

 dart run bin/server.dart --port 8081
Enter fullscreen mode Exit fullscreen mode

3) Define the port to be used programmatically :

 var portStr = result['port'] ?? Platform.environment['PORT'] ?? '8080';
Enter fullscreen mode Exit fullscreen mode

If we haven't passed the port number through the CLI, we check if we are using any Hosting Service (since they have their own ports) and use the Port defined by its Environment Variable. Still if it returns false (ie, we are in debug mode) we use our custom port of choice (Here, 8080).

4) Handle Invalid Port Numbers :

if (port == null) {
    stdout.writeln('Could not parse port value "$portStr" into a number.');
    // 64: command line usage error
    exitCode = 64;
    return;
  }
Enter fullscreen mode Exit fullscreen mode

5) Now the fun part! We define the Pipeline which the server has to use to receive requests and send responses to the caller.

var handler = const shelf.Pipeline()
      .addMiddleware(shelf.logRequests())
      .addHandler(_echoRequest);
Enter fullscreen mode Exit fullscreen mode

We use a Middleware defined by the shelf package to log the requests incoming to the server, and we define a Handler method to handle the incoming requests.
The Handler takes appropriate action based on the request method and returns some data.

6) Serve the Server :

var server = await io.serve(handler, _hostname, port);
Enter fullscreen mode Exit fullscreen mode

We attach the Pipeline, the Host Name, and the Port Number to the server and make it functional. Now, your Server is ready for action.

But wait! We have just one more thing left before we hit the RUN Button.

We have to define the Handler Method.

shelf.Response _echoRequest(shelf.Request request) =>
    shelf.Response.ok('Request for "${request.url}"');
Enter fullscreen mode Exit fullscreen mode

It returns an object of type Response and takes in an argument of type Request.

Does this architecture ring a bell for you ? 🔔

Yes, its our normal Web Server Architecture where we send Requests from the browser and the server returns a Response.

So, every time you call this program (ie, by using GET/POST/DELETE etc), this particular method is called and further processing happens from here. That's why we have attached this method as a Handler in the Server Pipeline.

shelf.Response.ok('Request for "${request.url}"');
Enter fullscreen mode Exit fullscreen mode

This line says that "We have received your request and its perfect, so we are giving you a 200 Status Code with the data 'Request for ${request.url}'"

Server Response after GET

See that wasn't so bad right?

Chandler Dancing

Now, lets connect our Supabase DB to our Dart Code.

We need to use the official Supabase Dart package :

https://pub.dev/packages/supabase

Package details

Now, open server.dart and import the supabase package :

import 'package:supabase/supabase.dart';
Enter fullscreen mode Exit fullscreen mode

Now, create a new method which will act as the Handler method when we call the url /users :

Future<shelf.Response> _echoUsers(shelf.Request request) async{
  final client = SupabaseClient('<SUPABASE URL>', '<SUPABASE KEY>');

  // Retrieve data from 'users' table
  final response =  await client
      .from('users')
      .select()
      .execute();

  var map = {
    'users' : response.data
  };

  return shelf.Response.ok(jsonEncode(map));
}
Enter fullscreen mode Exit fullscreen mode

The Supabase URL and Supabase Key can be found from your Settings page in Supabase Dashboard.

Pic of dashboard

anon public is the Supabase Key

The table created in Supabase DB is named users and we have used the same table to select all rows in the table and return them into the variable response. The query returns a list of rows which is directly stored into the users key of the map variable, and then returned as response for the API call.

Now, we need to add the routing. Remember, in the last section i had mentioned that _echoRequest() method is called every time we hit the API. So, once we reach there, we need to route to the correct Handler method based on the API URL. ie, If we enter /api we should call the Handler method defined for that request. Similarly, here we are calling /users, we need to route to the method _echoUsers.

So, we change the code inside _echoRequest as follows :

Future<shelf.Response> _echoRequest(shelf.Request request) async{
  switch(request.url.toString()) {
    case 'users': return _echoUsers(request);
    default : return shelf.Response.ok('Invalid url');
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, every time you hit the url /users, it will call the _echoUsers method in which we pass the request object.
If we try to access any other urls, a message "Invalid url" will be returned to the user.

But, i missed something! How does the code request.url.toString() work ?

Let me explain it with an example. Imagine you typed the following url in the browser : http://localhost:8080/users, request.url.toString() will return 'users'.
Similarly, if you typed the url : http://localhost:8080/users/aswin/profile, request.url.toString() will return 'users/aswin/profile'.

So, basically that snippet of code will return anything after http://localhost:8080/.

Well, now i see you have become an expert in building and deploying a full-fledged API in your local machine. But, trust me there are lot of Middleware packages available on pub.dev that extends the capability of the shelf package.

I will share more details at the end of this article. But before that, we have one more task left. Right?

Yes! Lets host this and make this API available to everyone!!

Hosting with Heroku

This section will take just 5 mins of your time. And in the next 10 mins your API will be live!

1) Head over to the Heroku Website and login using your credentials.

2) Create a new app from your dashboard.
Create new app button
Enter app name and select region

3) Your app dashboard should look like this :
Dashboard of newly created app

4) Next, Download and install the Heroku CLI

5) From the CLI, login into your Heroku account if you haven't yet.

$ heroku login
Enter fullscreen mode Exit fullscreen mode

6) Initialize a Git Repository. Type the following Commands while inside the root directory of the project.

$ cd my-project/
$ git init
$ heroku git:remote -a <heroku-app-name>
Enter fullscreen mode Exit fullscreen mode

7) Now, we need to initialize the Build pack which will be used by heroku to launch the dart app.
Check out the build pack we are gonna use on GitHub

Type the following command :

$ heroku buildpacks:set https://github.com/igrigorik/heroku-buildpack-dart.git
Enter fullscreen mode Exit fullscreen mode

8) Next, we need to set the Environment Variables in our Heroku Console. Head to Dashboard->Settings->Config Vars-> Reveal Config Vars and add the following key and value :

key : DART_SDK_URL
value : Link to the Dart SDK zip file for Linux from the Official website

At the time of writing this article, this was the link to the latest stable version of Dart SDK for Linux : https://storage.googleapis.com/dart-archive/channels/stable/release/2.14.4/sdk/dartsdk-linux-x64-release.zip

You can go to the Official Dart SDK site and search for the x64 package for Linux.

9) Next, head over to your root director of your project and create a new file named Procfile without any extensions.
Add the following content to the file :

web: ./dart-sdk/bin/dart bin/server.dart
Enter fullscreen mode Exit fullscreen mode

This will automatically run the server.dart code when you run the public link.

10) Now, its time to push the code to heroku!

$ git push heroku master
Enter fullscreen mode Exit fullscreen mode

You will get the link to the public URL which you can use it to access the API and even share it with the community 💙

Hurray!!

Joey and Chandler celebrating

You just created your own API and hosted it on Heroku. Give yourself a pat in the back!!


If you are facing any issues anywhere, feel free to reach out to me on my handles :

Twitter : @GopinathanAswin

LinkedIn : Aswin Gopinathan


Don't go yet!

You might be thinking, where you can go ahead from here right?

Like i mentioned before, shelf is just a Web server Middleware, it requires a lot of other add-ons to make a Full-Fledged API. Some of the few add-ons are :

1) Shelf Router

2) Shelf Router Generator

3) Shelf Static

4) Shelf WebSocket

5) Shelf CORS Headers

6) Shelf Proxy

You can read more about these in Filip's Article : Shelf — Web Server with Dart

I guess its bye-bye time now :😔 , but i will be back soon with more articles as i learn something new!!

Happy coding!

Discussion (1)

Collapse
techwithsam profile image
Samuel Adekunle

Wow