The Backbone Dart Backend Framework
A CLI for generating Dart REST APIs using the Backbone framework. If you're looking for the framework package itself, click here.
NOTE: This framework is very early in its development. Use at your own risk 😎
A huge shoutout to the mason package, the open_api_forked package and the shelf package! This project uses them heavily in its implementation, and it would be much more challenging to build without them.
Getting Started
Installing
- Make sure you have Dart installed on your computer. If you have Flutter installed, you probably also have Dart installed. You can check by running:
dart --version
If it is not, you can download it here. Also, make sure the version is >= 2.12.0
;
- Install backbone by running
dart pub global activate backbone_cli
Usage
-
Create a new file called
openapi.yaml
. -
Define your API using the Open API Specification, or copy the example spec.
-
In the folder your spec resides, run the following command. This will generate a new Backbone project in the current directory.
backbone generate --new
-
When the command finishes, you should see three new folders.
-
As you update the spec, you can run the following command without
--new
to update the API:
backbone generate
For more options (including what directories you'd like generated code to reside in), run:
backbone help generate
The Generated Code
The backbone generator looks in your supplied openapi.yaml
and generates three dart packages for you automatically:
- The
backend
package. This is the code that will run on the server and server your API. - The
functions_objects
package. This contains objects used by the backend and frontend to communicate with each other. You can think of this as the bridge between the frontend and backend. - The
fontend_packages/[API_NAME]
package. This is a frontend wrapper around your API so it can be easily called from Flutter code.
The functions_objects
and frontend_packages/[API_NAME]
packages are completely generated by the generator, so you will never need to edit them manually.
The backend
package has a lot of code generated by backbone, but you will still have to write the functionality of your API.
Writing your API
After initial generation, you should see a file backend/lib/[API_NAME].dart
. This is where your API endpoint functions will go.
NOTE: Your functions only need to be exported from this file, not nessesarily written there directly. You can write your functions anywhere in the
backend
package, just make sure they get exported from this file.
The signature of the function you need to write is different depending on if the request contains a body and if there are parameters to the request. Instead of remembering the rules, check out the backend/functions.md
file. It lists all the functions your API expects to be exposed, and you can copy and paste them from there and into your source code.
Writing your endpoint functions
RequestObject
If your request contains a body, your function will receive an object called [OPERATION-ID]Request
. This is a simple Dart object that will contain the information passed to the request via the body.
RequestParameters
If your request has any parameters defined (from the path, query, or headers), your function will receive an object called [OPERATION-ID]Parameters
. This is a simple Dart object that will contain the parameters passed into the request.
ResponseObject
All requests must return a 200
response, and your functions must return a object of type [OPERATION-ID]Response
. It can also return a Future of that object instead if you'd like your function to be async.
RequestContext
The request context is passed to every function. It currently contains
rawRequest
: This is the shelf Request object, which contains all data about the request including headers and more.logger
: This is aRequestLogger
object that can be used to log requests. If running locally, it will print logs out to the console; if running on Google Cloud Run, it will format logs for Cloud Logging. You can use it like this:
Future<GetPuppiesResponse> getPuppies(RequestContext context) async{
context.logger.info('Getting puppies');
}
userId
: This is the id of the user making the request, if any. See Authentication for more details.dependency<T>()
This can be used to cache depencencies in your backend. See Dependency Caching for more details.
Other Topics
Testing your API
Since each endpoint is simply one function, you can easily unit test it using Dart's test package. Simply mock the request and request context (we prefer mocktail package for this, but anything should work).
Debugging your API
Since your API is just Dart code, you can debug it using the Dart debugging tools you're familiar with.
In fact, if you open to your backend
package in Visual Studio Code, in the "Run and Debug" tab, you can start debugging your API by just clicking run.
Authentication
Backbone supports authentication via JWTs automatically. To use it, you need to do two steps:
- In you
backend/lib/[API_NAME].dart
folder, implement the function:
Future<String> verifyToken(String token, RequestContext context) async {
// TODO: implement verifyToken
}
It doesn't matter how you implement this function, but it should return a String?
that contains the userId if the token is valid. If the token is invalid, it should throw an AuthenticationException
.
- In your
openapi.yaml
file, add the following to thecomponents
section:
securitySchemes:
[AUTH_SCHEME_NAME]:
type: http
scheme: bearer
bearerFormat: JWT
Then, in each operation, add the following:
security:
- [AUTH_SCHEME_NAME]: []
- When creating your frontend object, pass the
authToken
into the constructor.
Authorization
You should authorize the users in the functions you write (backbone doesn't support it directly). If a user is not authorized, the function should throw an AuthorizationException
.
Dependency Caching
You can use the RequestContext
object to inject dependencies in your functions.
Example
Future<GetPetsResponse> getPets(RequestContext context) async {
final db = await context.dependency<FirestoreDatabase>(
() => createAndInitializeDatabase(),
);
}
All you need to do is call context.dependency<TYPE>(builder)
. The builder
is a function that returns your dependency. The trick is, it only re-builds your dependency if it hasn't been built yet. For example
print(await context.dependency<String>(() => 'test1'));
print(await context.dependency<String>(() => 'test2'));
print(await context.dependency<String>(() => 'test3'));
Will print
test1
test1
test1
because the dependency for type String
is set once the first time, and ignored the next two times. This is great if you have a database object that is expensive to initialize. You can initialize it once the first time you need it, and then re-use any subsequent times it's needed.
If you would like to force Backbone to reset the dependency, just add the force: true
option to the function call.
Example
print(await context.dependency<String>(() => 'test1'));
print(await context.dependency<String>(() => 'test2', force: true));
print(await context.dependency<String>(() => 'test3', force: true));
Will print
test1
test2
test3
You can also reset all depencencies in your API by calling resetDepencencies()
. This is great for testing because you can create a clean-slate environment for your tests.
Note: While we think it's a great solution to this problem, since it's all just Dart code, you can use whatever pattern you'd like.
Middleware
Backbone fully supports adding middleware to your server. From your top level backend/lib/[API_NAME].dart
file, export a list of middleware named middlewares
you'd like to use in the order you'd like them to process. For example:
final middlewares = <Middleware>[
fancyLoggingMiddleware(),
corsMiddleware(),
];
Deploying your API
The Backbone generator will create a Dockerfile
automatically for you (in backend/Dockerfile
) when running backbone generate --new
. You can build the Dockerfile by running
docker build . -f backend/Dockerfile -t [API_NAME]
You can also use it with Google Cloud Run, which is probably the easiest way to deploy your API.
Future Tasks
x
Auto-generate Dockerfile for deploymentx
Support for dependency injection using the request contextx
Support for middlewarex
Support for authentication