Building a Social Network: Part III

Creating a REST API Framework with Dart and PostgreSQL

Kenneth Reilly
ITNEXT

--

Animated vector illustration of software design block diagram
Animated illustration of a software design block diagram

Introduction

This article expands on Part I and Part II of this series (covering schema, security, and type definitions) with the creation of a REST API framework that will facilitate development of REST endpoints.

The framework is designed to reduce boilerplate code, allow for ease of maintenance and feature development, and integrate with underlying features developed in PL/pgSQL. Common requirements such as REST URL parsing, serialization, and authentication are built into classes that can either be extended to leverage functionality, or applied to methods on other classes to change the input, output, or other behavior using annotations.

This approach leverages concepts within OOP, @OP, meta-programming, and reflection to create software which is easy to describe and maintain, and Test-Driven Development is utilized to ensure high quality results.

For a copy of the source code for this project, clone this repo.

Package Definition

The server-side SDK package file is api_sdk/pubspec.yaml:

This SDK uses core from the previous article, and also makes use of dotenv, jaguar_jwt, and a few other standard packages.

The components of the SDK are exported from api_sdk/api-sdk.dart:

API Server

The base class for serving an API is in lib/api-server.dart:

This implements a basic server using the API framework which we will examine throughout this article. Environment variables are used to configure and initialize the authentication and database providers. Requests are forwarded to the APIService class, and the flush operation is called once every ten minutes to purge content older than 24 hours.

API Method Annotations

Let’s take a look next at lib/framework/api-method.dart:

When implementing an API, these classes are used as annotations to:

  • set a root URL on a service with RoutePath
  • designate REST and WebSocket endpoints
  • employ JSON serialization on a REST endpoint

Notice that an implementation for the + operator has been provided on RoutePath to simplify the concatenation of URL paths.

API Route

The class for internal route handling is in lib/framework/api-route.dart:

The RoutePoint and RouteParameter classes extend RouteComponent to build URL parsing logic from REST paths on each method (i.e. /users/:id in which the value of :id in a URL becomes the value of the id variable).

Instances of APIRoute are created by services to wrap methods that are annotated with one of the API methods listed above — such as GET or POST — and incoming requests are tested with the check function of each route until the correct route is found. Once the service has found a matching APIRoute for a request, the corresponding class method on the service is invoked using the MethodMirror along with positional and named arguments for the method (which are extracted from the incoming request using the _args and _parse functions).

API Service

The service base class is in lib/framework/api-service.dart:

When implementing an API using this framework, a class that extends APIService is created for each service, include annotations for the RoutePath base URL and APIRoute REST verb with the URL to be appended to the base.

When an instance of a service that extends APIService is created, reflection is used to retrieve information such as the base URL from RoutePath and theAPIRoute REST verb and path. For every method on the class that is designated as a REST or WS operation, an instance of APIRoute is created and stored in the_routes property. On incoming requests, each route is checked against the URL pattern until the correct route is found and invoked.

Reflector

Automatic serialization of data models by the SDK is assisted by the abstract Reflector class located in lib/types/reflector.dart:

The Reflector contains a set of utility methods that handle inspection of objects for serialization, along with casting JSON decoded Map<String, dynamic> objects to an instance of type T which allows the API to create instances of any Serializable object from JSON POST data.

AuthProvider

Authentication is handled within lib/services/auth-provider.dart:

Any endpoint that is annotated with the Authenticate decorator will be secured with a standard JWT authentication scheme. Any API that requires login and authentication can make use AuthProvider to tokenize an AuthenticatedUser and use this token to authorize subsequent API calls.

DataProvider

The DB connection is handled in lib/framework/data-provider.dart:

The DataProvider class wraps an instance of DB which in turn wraps the database connection itself, which is configured from environment variables passed in from the controlling server. The convenience method findOne is provided to return a single item from a result set where one is expected.

Test Service

An example service is included to demonstrate how a REST service is defined with the SDK, in lib/services/test-service.dart:

The @RoutePath annotation defines this service’s root URI path as '/' and @GET defines the URI for each method and will be used to automatically wire up these endpoints to respond to the HTTP GET verb. The @JSON annotation instructs the SDK to use JSON serialization and set the appropriate content-type on the response header.

SDK Test Base

The test framework of the SDK is built into the SDK itself and exported along with the rest of the library to enable code re-use in subsequent API tests.

The base class for this is found in lib/test/sdk-test-base.dart:

The SDKTestBase class includes a few methods for creating and starting a server instance, in addition to methods that will be used by test cases to make requests to the server and verify correct output.

Test Runner

A basic test implementation is found in test/test-runner.dart:

The test runner sets up an SDKTest with configuration and services under test. Basic operations such as URL parsing and object serialization are verified to be working properly, and future APIs built with this library will implement similar tests built upon this framework, ensuring continuity and reliability.

Conclusion

This API framework will serve as the basis for implementing various APIs required by the demo social network, such as mobile and/or web clients, administrative tools, and other services. The use of reflection and annotations greatly reduces boilerplate required for each service, as demonstrated by the TestService class above.

Stay tuned for Part IV in which we will build an API for creating users and interacting with other users on the system.

剣一

--

--