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:
Worry not when i am here!
In this tutorial you are gonna learn just two things :
- Create your own API with Dart
- 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 !
So, i have created a Database table in Supabase with the following content :
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!
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
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
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}"');
I know you will be like :
So, Let's break it down and understand each line.
1) Defining the Host Name :
const _hostname = 'localhost';
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
3) Define the port to be used programmatically :
var portStr = result['port'] ?? Platform.environment['PORT'] ?? '8080';
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;
}
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);
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);
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}"');
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}"');
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}'"
See that wasn't so bad right?
Now, lets connect our Supabase DB to our Dart Code.
We need to use the official Supabase Dart package :
https://pub.dev/packages/supabase
Now, open server.dart
and import the supabase package :
import 'package:supabase/supabase.dart';
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));
}
The Supabase URL and Supabase Key can be found from your Settings page in Supabase 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');
}
}
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.
3) Your app dashboard should look like this :
4) Next, Download and install the Heroku CLI
5) From the CLI, login into your Heroku account if you haven't yet.
$ heroku login
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>
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
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
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
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!!
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
3) Shelf Static
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!
Top comments (5)
Very nice and informative article! Thank you
at last what should be the hostname?
If you are hosting the code on heroku, change it to 0.0.0.0
Thanks a lot!!
Wow