Bones_API
Bones_API - A powerful API backend framework for Dart. It comes with a built-in HTTP Server, route handler, entity handler, SQL translator, and DB adapters.
Usage
A simple BTC-USD API example:
import 'package:bones_api/bones_api_server.dart';
import 'package:mercury_client/mercury_client.dart';
/// APIs are organized in modules:
class MyBTCModule extends APIModule {
MyBTCModule(APIRoot apiRoot) : super(apiRoot, 'btc');
/// The default route for not matching routes:
@override
String? get defaultRouteName => '404';
/// A configuration property from `apiConfig`.
String get notFoundMsg => apiConfig['not_found_msg'] ?? 'Unknown route!';
@override
void configure() {
routes.get('usd', (request) => fetchBtcUsd());
routes.any('time', (request) => APIResponse.ok(DateTime.now()));
routes.any('404', notFound);
}
/// A HTTP client for `fetchBtcUsd`:
static final coinDeskClient = HttpClient("https://api.coindesk.com/v1/bpi");
/// Fetches the BTS-USD price.
Future<APIResponse<num>> fetchBtcUsd() async {
var response = await coinDeskClient.get('currentprice.json');
if (response.isNotOK) {
return APIResponse.notFound();
}
var btcUsd = response.json['bpi']['USD']['rate_float'] as num?;
return btcUsd != null ? APIResponse.ok(btcUsd) : APIResponse.notFound();
}
/// Not found route (`404`):
FutureOr<APIResponse> notFound(request) {
// The requested path:
var path = request.path;
var body = '''
<h1>404</h1><br>
<b>PATH:<b> $path
<p>
<i>$notFoundMsg</i>
''';
// `APIResponse` with `content-type` and `cache-control`:
return APIResponse.notFound(payload: body)
..payloadMimeType = 'text/html'
..headers['cache-control'] = 'no-store';
}
}
/// The `APIRoot` defines the API version and modules to use:
class MyAPI extends APIRoot {
MyAPI({dynamic apiConfig}) : super('example', '1.0', apiConfig: apiConfig);
// Load the modules used by this API:
@override
Set<APIModule> loadModules() => {MyBTCModule(this)};
}
/// Starts an [APIServer] and calls the routes through [HttpClient]:
void main() async {
// A JSON to configure the API:
var apiConfigJson = '''
{"not_found_msg": "This is 404!"}
''';
var api = MyAPI(apiConfig: apiConfigJson);
int? serverPort = await startAPIServer(api);
var httpClient = HttpClient("http://localhost:$serverPort/");
var btcUsd = (await httpClient.get('/btc/usd')).bodyAsString;
print('BTC-USD: $btcUsd');
var time = (await httpClient.post('/btc/time')).bodyAsString;
print('TIME: $time');
var foo = (await httpClient.get('/btc/foo')).bodyAsString;
print('FOO:\n$foo');
await stopAPIServer();
}
late final APIServer apiServer;
/// Starts the [APIServer] (HTTP Server) and returns the port.
/// - With Hot Reload if `--enable-vm-service` is passed to the Dart VM.
Future<int?> startAPIServer(MyAPI api) async {
var serverPort = 8088;
print('Starting APIServer...\n');
apiServer = APIServer(api, '*', serverPort, hotReload: true);
await apiServer.start();
print('\n$apiServer');
print('URL: ${apiServer.url}\n');
return serverPort;
}
/// Stops the [APIServer].
Future<bool> stopAPIServer() async {
await apiServer.stop();
return true;
}
OUTPUT:
Starting APIServer...
2021-10-08 02:15:17.924328 [CONFIG] (main) APIHotReload > pkgConfigURL: ~/workspace/bones_api/.dart_tool/package_config.json
2021-10-08 02:15:17.959068 [CONFIG] (main) APIHotReload > Watching [~/workspace/bones_api] with [MacOSDirectoryWatcher]...
2021-10-08 02:15:18.185128 [INFO] (main) APIHotReload > Created HotReloader
2021-10-08 02:15:18.185624 [INFO] (main) APIHotReload > Enabled Hot Reload: true
2021-10-08 02:15:18.185852 [INFO] (main) APIServer > Started HTTP server: 0.0.0.0:8088
APIServer{ apiType: MyAPI, apiRoot: example[1.0]{btc}, address: 0.0.0.0, port: 8088, hotReload: true, started: true, stopped: false }
URL: http://0.0.0.0:8088/
BTC-USD: 53742.76
TIME: 2021-10-08 02:15:18.294076
FOO:
<h1>404</h1><br>
<b>PATH:<b> /btc/foo
<p>
<i>This is 404!</i>
CLI
You can use the built-in command-line interface (CLI) bones_api.
To activate it globally:
$> dart pub global activate bones_api
Now you can use the CLI directly:
$> bones_api --help
To serve an API project:
$> bones_api serve --directory path/to/project --class MyAPIRoot --config api-prod.conf --port 80 --address 0.0.0.0 --build --hotreload --domain mydomain.com=/var/www
To create an API project file tree:
$> bones_api create -o /path_to/workspace/foo_api -p project_name_dir=foo_api -p "project_name=Foo API" -p "project_description=API for Foo stuffs." -p homepage=http://foo.com
Hot Reload
APIServer supports Hot Reload when the Dart VM is running with --enable-vm-service:
void main() async {
var apiServer = APIServer(api, 'localhost', 8080, hotReload: true);
await apiServer.start();
}
The CLI bones_api, when called with --hotreload,
will launch a new Dart VM with --enable-vm-service (if needed) to allow Hot Reload.
To serve an API project with Hot Reload enabled:
$> bones_api serve --directory path/to/project --class MyAPIRoot --hotreload
Using Reflection
You can use the package reflection_factory to automate some declarations.
For example, you can map all routes in a class with one line of code:
File: module_account.dart:
import 'package:bones_api/bones_api.dart';
import 'package:reflection_factory/reflection_factory.dart';
// See Repositories sections below in this README:
import 'repositories.dart';
// The generated reflection code by `reflection_factory`:
part 'module_account.reflection.g.dart';
@EnableReflection()
class AccountModule extends APIModule {
AccountModule(APIRoot apiRoot) : super(apiRoot, 'account');
final AddressAPIRepository addressRepository = AddressAPIRepository();
final AccountAPIRepository accountRepository = AccountAPIRepository();
@override
void configure() {
// Maps the POST routes by reflection of any method in this class
// that returns `APIResponse` or accepts `APIRequest`.
routes.postFrom(reflection);
}
// The request parameters will be mapped to the correct
// method parameter by name:
Future<APIResponse> auth(String? email, String? password) async {
if (email == null) {
return APIResponse.error(error: 'Invalid parameters!');
}
if (password == null) {
return APIResponse.unauthorized();
}
var sel = await accountRepository.selectAccountByEmail(email);
if (sel.isEmpty) {
return APIResponse.unauthorized();
}
var account = sel.first;
// The object `account` will be automatically converted
// to JSON when the response is sent through HTTP.
return account.checkPassword(password)
? APIResponse.ok(account)
: APIResponse.unauthorized();
}
}
Declaring Entities & Reflection
You can declare entities classes in portable Dart code (that also works in the Browser).
To easily enable toJSon and fromJson, just add @EnableReflection() to your entities.
File: entities.dart:
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:reflection_factory/reflection_factory.dart';
part 'entities.reflection.g.dart';
@EnableReflection()
class Account {
int? id;
String email;
String passwordHash;
Address? address;
Account(this.email, String passwordOrHash, this.address, {this.id})
: passwordHash = hashPassword(passwordOrHash);
Account.create() : this('', '', null);
bool checkPassword(String password) {
return passwordHash == hashPassword(password);
}
static final RegExp _regExpHEX = RegExp(r'ˆ(?:[0-9a-fA-F]{2})+$');
static bool isHashedPassword(String password) {
return password.length == 64 && _regExpHEX.hasMatch(password);
}
static String hashPassword(String password) {
if (isHashedPassword(password)) {
return password;
}
var bytes = utf8.encode(password);
var digest = sha256.convert(bytes);
var hash = digest.toString();
return hash;
}
}
@EnableReflection()
class Address {
int? id;
String countryCode;
String state;
String city;
String address1;
String address2;
String zipCode;
Address(this.countryCode, this.state, this.city, this.address1, this.address2,
this.zipCode,
{this.id});
Address.create() : this('', '', '', '', '', '');
}
See reflection_factory for more Reflection documentation.
Repositories & Database
To stored entities in Databases and manipulate them you can set up an EntityRepositoryProvider:
File: repositories.dart
import 'package:bones_api/bones_api.dart';
// Import the PostgreSQL Adapter:
import 'package:bones_api/bones_api_adapter_postgre.dart';
// Import the above entities file:
import 'entities.dart';
/// The API `EntityRepositoryProvider`:
class APIEntityRepositoryProvider extends EntityRepositoryProvider {
static final APIEntityRepositoryProvider _instance =
APIEntityRepositoryProvider._();
// Singleton:
factory APIEntityRepositoryProvider() => _instance;
// Returns the current `APIRoot`:
APIRoot? get apiRoot => APIRoot.get();
APIEntityRepositoryProvider._() {
// The current APIConfig:
var apiConfig = apiRoot?.apiConfig;
var postgreAdapter = PostgreSQLAdapter.fromConfig(
apiConfig?['postgres'], // The connection configuration
parentRepositoryProvider: this,
);
// Join the `PostgreSQLAdapter` and the Address/Account
// `EntityHandler` (from reflection) to set up an
// `EntityRepository` that uses SQL:
// Entity `Address` in table `address`:
SQLEntityRepository<Address>(
postgreAdapter, 'address', Address$reflection().entityHandler);
// Entity `Account` in table `account`:
SQLEntityRepository<Account>(
postgreAdapter, 'account', Account$reflection().entityHandler);
}
}
/// The [Address] APIRepository:
class AddressAPIRepository extends APIRepository<Address> {
AddressAPIRepository() : super(provider: APIEntityRepositoryProvider());
/// Selects an [Address] by field `state`:
FutureOr<Iterable<Address>> selectByState(String state) {
return selectByQuery(' state == ? ', parameters: {'state': state});
}
}
/// The [Account] APIRepository:
class AccountAPIRepository extends APIRepository<Account> {
AccountAPIRepository() : super(provider: APIEntityRepositoryProvider());
/// Selects an [Account] by field `email`:
FutureOr<Iterable<Account>> selectAccountByEmail(String email) {
return selectByQuery(' email == ? ', parameters: {'email': email});
}
/// Selects an Account by field `address` and sub-field `state`:
FutureOr<Iterable<Account>> selectAccountByAddressState(String state) {
// This condition will be translated to a SQL with INNER JOIN (when using an SQLAdapter):
return selectByQuery(' address.state == ? ', parameters: [state]);
}
}
The config file used above:
File: api-local.yaml
postgres:
database: yourdb
username: postgres
password: 123456
SQLAdapter
To use a SQL database with your EntityRepository you need a SQLAdapter:
PostgreSQLAdapter: a PostgreSQL adapter.MySQLAdapter: A MySQL adapter.MemorySQLAdapter: a portableSQLAdapterthat stores entities in memory.
The SQLAdapter is responsible to connect to the database, manage the connection
pool and also to adjust the generated SQLs to the correct dialect.
Bones_UI
See also the package Bones_UI, a simple and easy Web User Interface Framework for Dart.
Features and bugs
Please file feature requests and bugs at the issue tracker.
Contribution
Any help from the open-source community is always welcome and needed:
- Found an issue?
- Please fill a bug report with details.
- Wish a feature?
- Open a feature request with use cases.
- Are you using and liking the project?
- Promote the project: create an article, do a post or make a donation.
- Are you a developer?
- Fix a bug and send a pull request.
- Implement a new feature.
- Improve the Unit Tests.
- Have you already helped in any way?
- Many thanks from me, the contributors and everybody that uses this project!
Author
Graciliano M. Passos: gmpassos@GitHub.
License
Libraries
- bones_api
- Bones_API Library.
- bones_api_console
- Bones_API Console Library.
- bones_api_dart_spawner
- Bones_API Server Library.
- bones_api_db_directory
- Bones_API DB Directory Adapters.
- bones_api_db_gcp
- Bones_API DB Adapters for Google Cloud Platform (GCP).
- bones_api_db_mysql
- Bones_API DB Adapter for MySQL.
- bones_api_db_postgre
- Bones_API DB Adapter for PostgreSQL.
- bones_api_logging
- Bones_API Logging Library.
- bones_api_server
- Bones_API Server Library.
- bones_api_test
- Bones_API Test Library.
- bones_api_test_mysql
- Bones_API Test MySQL Library.
- bones_api_test_postgres
- Bones_API Test PostgresSQL Library.
- bones_api_test_vm
- Bones_API Test VM Library.