Building a Social Network: Part III
Creating a REST API Framework with Dart and PostgreSQL
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
andWebSocket
endpoints - employ
JSON
serialization on aREST
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.