Building Dart APIs with Google Cloud Run
Learn how to build backend applications using Dart and Google Cloud Run. By Alhassan Kamil.
        
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Building Dart APIs with Google Cloud Run
30 mins
- Getting Started
 - Running Your First Dart API Server
 - Setting up a Google Cloud Project
 - Creating the Project
 - Creating a Service Account
 - Setting up Billing
 - An Overview of REST APIs
 - Developing the API Endpoints
 - Designing Endpoints
 - Implementing the UserRoutes Endpoints
 - Adding the NoteRoutes Endpoints
 - Creating the AppRoutes Endpoints
 - Testing the Routes
 - Writing Controller Logic
 - UserController
 - NoteController
 - Adding Middleware
 - Response Headers Middleware
 - Authentication Middleware
 - Deploying Your Dart API on Google Cloud Run
 - Installing Google Cloud CLI
 - Configuring the Dockerfile
 - Deploying to Cloud Run
 - Accessing Your Dart REST API
 - Where to Go From Here
 
Writing Controller Logic
A controller, borrowed from the MVC pattern, is a class that sits between the client and the resource server (the database, in this case) and controls what data the client can access and what format it’s in for the client to understand.
You’ll implement two controllers: UserController and NoteController. UserController holds authentication logic while NoteController contains the logic for managing notes.
UserController
You’ll use register() and login() in lib/controllers/user_controller.dart to authenticate users. These have been defined, so you just need to modify them going forward.
Import these in lib/controllers/user_controller.dart:
import 'dart:convert';
import 'package:collection/collection.dart';
import '../helpers/helper.dart';
import '../models/user.dart';
Next, change register() so it contains the following:
// 1
final req = await request.readAsString();
// 2
if (request.isEmpty || !validate(req)) {
  return Response.forbidden(jsonEncode({'message': 'Bad request'}));
}
// 3
final mJson = jsonDecode(req) as Map<String, dynamic>;
final apiKey = Helper.randomChars(40);
final id = Helper.randomChars(15);
final user = User(
    id: id,
    email: (mJson['email'] ?? '') as String,
    password: Helper.hash(mJson['password'] as String),
    apiKey: apiKey);
try {
  // 4
  Helper.push(firestoreApi,
          path: 'users/$id',
          fields: user
          .toMap()
          .map((key, value) => MapEntry(key, Value(stringValue: value))));
  return Response.ok(user.toJson());
} on Exception {
// 5
  return Helper.error();
}
This is what your code is doing:
- Reading the request body into 
reqas a string. - Returning a 
403 Forbiddenresponse if the request is empty, has no body or eitheremailorpasswordare missing from the request body. - If the request passes validation, it generates an 
apiKeyandid, then uses it with a hash of the user’s password to create a user. - Saving the user against their ID to the users collection on Cloud Firestore, wrapping this code in a 
tryblock to capture any exceptions thrown, and returning a200 OKresponse if everything goes well. - Returning a 
503 Internal Server Errorresponse if an exception was thrown, signalling an unsuccessful resource creation. 
Notice that you called validate() in register(), which hasn’t been implemented yet. Modify it’s stub to contain the code below:
final json = jsonDecode(req) as Map;
return req.trim().isNotEmpty &&
    json['email'] != null &&
    (json['email'] as String).trim().isNotEmpty &&
    json['password'] != null &&
    (json['password'] as String).trim().isNotEmpty;
All validate() does is to validate that req contains both email and password.
Finish up UserController by modifying login() to have the following code:
final req = await request.readAsString();
if (request.isEmpty || !validate(req)) {
  return Response.forbidden(jsonEncode({'message': 'Bad request'}));
}
final mJson = jsonDecode(req) as Map<String, dynamic>;
// 1
final docs = await Helper.getDocs(firestoreApi, 'users');
if ((docs.documents ?? []).isEmpty) {
  return Response.notFound(jsonEncode({'message': 'User not found'}));
}
// 2
final user = docs.documents!.firstWhereOrNull((e) =>
    e.fields?['email']?.stringValue == mJson['email'] &&
    e.fields?['password']?.stringValue ==
       Helper.hash(mJson['password'] as String));
if (user == null) {
  return Response.forbidden(
      jsonEncode({'message': 'Invalid email and/or password'}));
}
return Response.ok(jsonEncode(
    {'apiKey': docs.documents!.first.fields?['apiKey']?.stringValue}));
The code above:
- Fetches all users from Cloud Firestore using 
Helper.getDocs(). This utility function takes aFirestoreApiinstance and the collection ID to fetch from, users in the case above. - Finds a user with the email-password combination, returning a 
200 OKresponse if they exists or403 Forbiddenif they don’t. 
Restart the server and see that authentication works.
Register a user:
curl -X POST -d '{"email": "newuser@example.com", "password": "pass1234"}'  http://localhost:8080/v1/users/register
Then log the user in:
curl -X POST -d '{"email": "newuser@example.com", "password": "pass1234"}' http://localhost:8080/v1/users/login
NoteController
The NoteController manages notes. For now, it contains stubs for store(), index(), show() and destroy(), which you’ll change in this section.
Firstly, you need to import the following:
import 'dart:convert';
import '../helpers/helper.dart';
import '../models/note.dart';
Then, modify store() to contain the following code:
// 1
final req = await request.readAsString();
final id = Helper.randomChars(15);
final isEmpty = request.isEmpty || req.trim().isEmpty;
if (isEmpty) {
  return Response.forbidden(jsonEncode({'message': 'Bad request'}));
}
// 2
final json = jsonDecode(req) as Map<String, dynamic>;
final title = (json['title'] ?? '') as String;
final description = (json['description'] ?? '') as String;
if (title.isEmpty || description.isEmpty) {
  return Response.forbidden(
    jsonEncode({'message': 'All fields are required'}));
}
// 3
final note = Note(title: title, description: description, id: id);
try {
  await Helper.push(firestoreApi,
      path: 'notes/$id',
      fields: note
          .toMap()
          .map((key, value) => MapEntry(key, Value(stringValue: value))));
  return Response.ok(note.toJson());
} on Exception {
  // 4
  return Helper.error();
}
This is what your code is doing:
- Checking the request and returning a 
403 Forbiddenresponse if it’s empty. - Decoding the request and returning another 
403 Forbiddenresponse if eithertitleordescriptionis empty. - Creating a new note, saving it into the notes collection and returning 
200 OKsuccess response containing the note. - Returning a 
503 Internal Server Errorif the note was unable to save. 
The next task is to retrieve all notes in index(). So alter it’s body with the following code:
try {
  final docList = await Helper.getDocs(firestoreApi, 'notes');
  final notes = docList.documents
      ?.map((e) =>
         e.fields?.map((key, value) => MapEntry(key, value.stringValue)))
      .toList();
  return Response.ok(jsonEncode(notes ?? <String>[]));
} on Exception {
  return Helper.error();
}
The code above retrieves all notes from the database and returns them as a response. If there was an exception, an error response is, instead, returned.
Moving forward, change the body of show() to also match the code below:
try {
  final doc = await firestoreApi.projects.databases.documents
      .get('${Helper.doc}/notes/$id');
  final notes =
      doc.fields?.map((key, value) => MapEntry(key, value.stringValue));
  return Response.ok(jsonEncode(notes));
} on Exception {
  return Helper.error();
}
This retrieves the note by it’s ID and returns a 200 OK response containing the note or a 503 Internal Server Error response if there’s an error.
Lastly, you’d want to delete notes. You can do that in destroy() like this:
try {
  await firestoreApi.projects.databases.documents
      .delete('${Helper.doc}/notes/$id');
  return Response.ok(jsonEncode({'message': 'Delete successful'}));
} on Exception {
  return Helper.error();
}
The code is similar to that of show(), except you used delete() instead of get() and returned a message instead of a note.
This completes the note controller logic, so you can now manage notes. All API endpoints should now also work.
Restart the server and try to save a note:
curl -X POST -d '{"title": "Monday Journeys","description": "Accra to Tamale"}' http://localhost:8080/v1/notes/
You should get a response like below:
Explore the other endpoints to see the responses:
- Get all notes 
curl http://localhost:8080/v1/notes. - Get the note with ID WVVehUGGr56RkXy: 
curl http://localhost:8080/v1/notes/WVVehUGGr56RkXy. - Delete note with ID WVVehUGGr56RkXy: 
curl -X DELETE http://localhost:8080/v1/notes/WVVehUGGr56RkXy. 
As of now, anyone can use the API to access notes. This isn’t a good security practice. So you need to make the notes accessible to only authenticated users. You’ll use middleware to do that.


