DEV Community

Cover image for Build APIs using Dart to CRUD Data in Supabase
Aswin Gopinathan
Aswin Gopinathan

Posted on

Build APIs using Dart to CRUD Data in Supabase

Most of the production-level APIs out there perform some common operations like:

  • Creating a new resource
  • Reading and displaying available resources
  • Updating an existing resource
  • Delete an existing resource

In this article, you are gonna learn how you can build your own API that does the above operations on your table in Supabase.

Note: Most of the operations above will require a server-side authentication and authorization, which we will not be covering in this article. This article will focus more on the operations than the underlying AuthN and AuthZ.
But, dont worry i will make sure i will write an article specifically on that topic in the near-future.

So, before we start with the coding part, go ahead and create a new table in your Supabase console.
Check out the Official documentation to know how you can create your own table in Supabase.

We will be using the following table for this article:
Database image

We will be using the shelf and the shelf_router package for building our API.
If you are new to these packages, i recommend reading my previous articles which will walk you through these in depth :

1. Create an API with Dart + Heroku
First Article Cover Pic

2. Build APIs for various HTTP Methods in Dart
Second Article Cover pic

After you create the template dart-server app, 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

We have to add dependencies for two packages in pubspec.yaml:

shelf_router: ^1.1.2
supabase: ^0.2.9
Enter fullscreen mode Exit fullscreen mode

Change the version number according to the latest one at the time of reading this article.

Next, we create a new file user.dart and create an API class as follows:

import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:supabase/supabase.dart';

class Users {
  final client = SupabaseClient('<Supabase-URL>', '<Supabase-Key>');

  Handler get handler {
    final router = Router(
      notFoundHandler: (Request request) => 
        Response.notFound(
          'We dont have an API for this request'
        )
    );

    /// Create new user

    /// Read all users

    /// Read user data with id

    /// Update user using Id

    /// Delete a user using ID



    return router;
  }
}
Enter fullscreen mode Exit fullscreen mode

This class will act as our routing class for the handlers based on request methods and URLs.

We create an object to the SupabaseClient using the supabase-url and supabase-key which we can get from our Supabase account.

handler is a getter which will return the Response object which will be displayed as output to the requester.

final router = Router(
    notFoundHandler: (Request request) => 
        Response.notFound(
          'We dont have an API for this request'
        )
    );

Enter fullscreen mode Exit fullscreen mode

Here, we create a Router object which will be used to connect various route requests based on Request Methods (GET, POST, DELETE) or URLs, to its appropriate handler methods.
The notFoundHandler: attribute specifies the Response to be shown in case a Request is made to an endpoint which we haven't defined a handler for.

Now, go back to server.dart file and update the code in serve() method to use this new handler getter method of Users class as the handler for our dart server.

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

Now, we are ready to write our handlers!

1. Create a new entry

Add the following code under the comment /// Create new user

router.post('/users/create', (Request request) async {
    final payload = jsonDecode(await request.readAsString());

    // If the payload is passed properly
    if(payload.containsKey('name') && payload.containsKey('age')) {

    // Create operation
    final res = await client
      .from('users')
      .insert([
        {'name': payload['name'], 'age': payload['age']}
      ]).execute();

      // If Create operation fails
      if(res.error!=null) {
        return Response.notFound(
          jsonEncode(
            {'success':false, 'data': res.error!.message,}
          ),
          headers: {'Content-type':'application/json'}
        );
      }

      // Return the newly added data
      return Response.ok(
        jsonEncode({
          'success':true,
          'data':res.data
        }),
        headers: {'Content-type':'application/json'},
      );
    }

    // If data sent as payload is not as per the rules
    return Response.notFound(
      jsonEncode(
        {'success':false, 'data':'Invalid data sent to API',}
        ),
      headers: {'Content-type':'application/json'}
    );

 });
Enter fullscreen mode Exit fullscreen mode

Since creating a new resource is a POST request, we use router.post() method and listen to the url /users/create. We have to pass a body to the endpoint which is then stored in the payload variable as a json data.

The syntax of the body should be:

{
    "name":"Aswin",
    "age":22
}
Enter fullscreen mode Exit fullscreen mode

Here, name and age corresponds to the respective column names in the users table in Supabase DB.

final res = await client
   .from('users')
   .insert([
   {'name': payload['name'], 'age': payload['age']}
]).execute();
Enter fullscreen mode Exit fullscreen mode

This snippet of code performs the Write operation to the DB. It inserts a new column with the given name and age, and returns some value to the variable res.

If the insert operation is successful, res.error will be null and res.data will contain the newly inserted row.
If the insert operation is unsuccessful, res.data will be null and res.error!.message will contain the error that caused the failure.

So, based on the above condition, appropriate Response objects are returned and displayed to the requester.

API Url :
/users/create
Method :
POST
Body :
{ "name":"Aswin", "age":22 }

2. Read all entries

Add the following code under the comment /// Read all users

router.get('/users', (Request request) async {
    final res = await client
      .from('users')
      .select()
      .execute();

    // If the select operation fails
    if(res.error!=null) {
      return Response.notFound(
        jsonEncode({
          'success':false,
          'data':res.error!.message
        }),
        headers: {'Content-type':'application/json'}
      );
    }

    final result = {
      'success':true,
      'data': res.data,
    };

    return Response.ok(
      jsonEncode(result),
      headers: {'Content-type':'application/json'}
    );
  });
Enter fullscreen mode Exit fullscreen mode

Since reading entries is a GET request, we have used router.get() method and it listens to the URL /users. It dosen't require a body.

The following code performs the read operation and returns the data :

final res = await client
  .from('users')
  .select()
  .execute();
Enter fullscreen mode Exit fullscreen mode

API Url:
/users
Method :
GET
Body:
NA

3. Read entry based on Id

Add the following code under the comment /// Read user data with id

router.get('/users/<id>', (Request request,String id) async {
      final res = await client
        .from('users')
        .select()
        .match({'id':id})
        .execute() ;

      // res.data is null if we pass a string as ID eg: 11a
      if(res.data == null) {
        return Response.notFound(
            jsonEncode({
              'success':false,
              'data':'Invalid ID'
            }),
            headers: {'Content-type':'application/json'}
        );
      }

      // res.data.length is 0 if an entry with given ID is not present
      if(res.data.length!=0) {
        final result = {
          'success':true,
          'data':res.data
        };

        return Response.ok(
          jsonEncode(result),
          headers: {'Content-type':'application/json'}
        );
      }
      else {
        return Response.notFound(
          jsonEncode({
            'success':false,
            'data':'No data available for selected ID'
          }),
          headers: {'Content-type':'application/json'}
        );
      }
    });
Enter fullscreen mode Exit fullscreen mode

We have set the url as /users/<id> where the user id to fetch will be passed in the API call, and based on the id, if found will return the user details, else, an error will be returned.

API Url:
/users/1
Method :
GET
Body:
NA

4. Update entry in DB

Add the following code below /// Update user using Id

router.put('/users/update/<id>', (Request request,String id) async {
    final payload = jsonDecode(await request.readAsString());

    final res = await client
      .from('users')
      .update(payload)
      .match({ 'id': id })
      .execute();

    // if update operation was successful
    if(res.data!=null) {
      final result = {
        'success':true,
        'data':res.data
      };

      return Response.ok(
        jsonEncode(result),
        headers: {'Content-type':'application/json'}
      );
    }

    // if update operation failed
    else if(res.error!=null) {

      // if the Id passed does not exist in the DB
      if(res.error!.message.toString() == '[]') {
        return Response.notFound(
            jsonEncode({
              'success':false,
              'data':'Id does not exist',
            }),
            headers: {'Content-type':'application/json'}
        );
      }

      // If any internal issue or the data passed is invalid
      return Response.notFound(
        jsonEncode({
          'success':false,
          'data':res.error!.message,
        }),
        headers: {'Content-type':'application/json'}
      );
    }
  });
Enter fullscreen mode Exit fullscreen mode

Since update calls are always PUT, we have used route.put() method and listens to the url /users/update/<id> where we pass the id of the user whose data we have to update.
So, since it requires new data, we have to pass the body as well while hitting the API.

The syntax of body can be:

{
    "name":"Aswin",
    "age":22
}
Enter fullscreen mode Exit fullscreen mode

You don't have to mention the entire fields (name and age) while updating, you just have to give only those fields whose value is being updated.

The following is the code that updates the data based on the id passed:

final res = await client
  .from('users')
  .update(payload)
  .match({ 'id': id })
  .execute();
Enter fullscreen mode Exit fullscreen mode

.match() searches for the entry with given id and updates only those fields which were send via the API call.

API Url:
/users/update/1
Method :
PUT
Body:
{"name":"Ross"}

5. Delete entry in DB

Add the following code below /// Delete a user using ID

router.delete('/users/delete/<id>', (Request request,String id) async {
  final res = await client
      .from('users')
      .delete()
      .match({'id':id})
      .execute();

  // if delete operation was successful
  if(res.data!=null) {
    if(res.data.toString() == '[]') {
      return Response.notFound(
        jsonEncode({
          'success':false,
          'data':'Id not found'
        }),
        headers: {'Content-type':'application/json'}
      );
    }
    final result = {
      'success':true,
      'data':res.data
    };

    return Response.ok(
        jsonEncode(result),
        headers: {'Content-type':'application/json'}
    );
  }

  // if delete operation failed
  else if(res.error!=null) {

    // if the Id passed does not exist in the DB
    if(res.error!.message.toString() == '[]') {
      return Response.notFound(
          jsonEncode({
            'success':false,
            'data':'Id does not exist',
          }),
          headers: {'Content-type':'application/json'}
      );
    }

    // If any internal issue or the data passed is invalid
    return Response.notFound(
        jsonEncode({
          'success':false,
          'data':res.error!.message,
        }),
        headers: {'Content-type':'application/json'}
    );
  }

});
Enter fullscreen mode Exit fullscreen mode

Since deleting a request is a DELETE request, we have used route.delete() method with the URL /users/delete/<id>.

The following code performs the delete operation in the Supabase DB:

final res = await client
    .from('users')
    .delete()
    .match({'id':id})
    .execute();
Enter fullscreen mode Exit fullscreen mode

It searches the user with the given id and performs the delete operation and returns the deleted data or the error, and accordingly response is sent back to the requester.

API Url:
/users/delete/1
Method :
DELETE
Body:
NA


Where can you go from here ?

I haven't provided any server-side AuthN and AuthZ, which could be an improvisation to this work. You can generate session ids for each call and use them to verify if the delete/update operation is authorized or not!


That's all folks

This article was just intended to strengthen your knowledge of connecting to a Realtime DB and perform CRUD using APIs built using our very own DART! 💙

We will meet in my next article 😎🙌🏻

Joey saying bye

Discussion (0)