We're very excited to announce that we've open sourced Dart Frog, an experimental, minimalistic backend framework for Dart 🎉
What is Dart Frog?
Our goal is to help developers effectively build backends in Dart. Dart Frog is intended to help Flutter and Dart developers maximize their productivity by having a unified tech stack that enables sharing tooling, models, and more!
Dart Frog provides a simple core with a small API surface area in order to reduce the learning curve and ramp-up time for developers. It optimizes the process of building backends which aggregate, compose, and normalize data from multiple sources.
Dart Frog is built on top of shelf and mason and is inspired by many tools including remix.run, next.js, and express.js.
Getting Started 🚀
The easiest way to get started is by installing the dart_frog_cli via pub.dev.
At this point, dart_frog should be available as a command. You can verify by running dart_frog in your terminal.
Create a Project 📦
Use the dart_frog create command to generate a brand new project.
Start the Dev Server 🏁
Next, we can open the newly created project and start the dev server via:
This will start the server on localhost:8080 with hot-reload enabled ⚡️
Creating a new Route 🚏
In Dart Frog, a route consists of an onRequest function (called a route handler) exported from a dart file in the routes directory. Each endpoint is associated with a routes file based on its filename. Files named, index.dart will correspond to a / endpoint.
For example, if you create routes/hello.dart that exports an onRequest method like below, it will be accessible via the /hello endpoint.
In addition, all route handlers have access to a RequestContext which can be used to access the incoming request as well as dependencies provided to the request context (see middleware below).
We can customize the status code of the response via the statusCode parameter on the Response object:
In addition, we can return JSON via the Response.json constructor:
Route handlers can be synchronous or asynchronous. To convert the above route handlers to async, we just need to update the return type from Response to Future<Response>. We can also add the async keyword in order to await futures within our handler before returning a Response.
Dynamic Routes 🌓
Dart Frog supports dynamic routes. For example, if you create a file called routes/posts/[id].dart, then it will be accessible at endpoints: /posts/1, /posts/2, etc.
Routing parameters are forwarded to the onRequest method as seen below.
Middleware in Dart Frog allows you to execute code before and after a request is processed. You can modify the inbound request and outbound responses, provide dependencies, and more! This can be useful for validating authorization, adding common headers, logging, etc.
In Dart Frog, a piece of middleware consists of a middleware function exported from a _middleware.dart file within a subdirectory of the routes folder. There can only ever be one piece of middleware per route directory with routes/_middleware.dart being middleware that is executed for all inbound requests.
We can chain built-in middleware, such as the requestLogger middleware via the use API. For example, if we create routes/_middleware.dart with the following contents, we will automatically log all requests to our server.
Dependency Injection 💉
Middleware can also be used to provide dependencies to a RequestContext via a provider.
provider is a type of middleware that can create and provide an instance of type T to the request context. The create callback is called lazily and the injected RequestContext can be used to perform additional lookups to access values provided upstream.
In the following example, we'll use a provider to inject a String into our request context.
We can later access the provided via from within a route handler using context.read<T>():
In Dart Frog, we can unit test our route handlers and middleware effectively because they are plain functions.
For example, we can test our route handler above using package:test:
In the above test, we're using package:mocktail to create a mock RequestContext and stub the return value when calling context.read<String>(). Then, all we need to do is call onRequest with the mocked context and we can assert that the response is what we expect. In this case, we're checking the statusCode and response body to ensure that the response is a 200 with the provided greeting.
Check out the Dart Frog Roadmap to see where we're headed.
If you're as excited about Dart Frog as we are, please let us know! We'd love to hear any feedback from the community to help shape our direction and look forward to seeing Dart becoming more prevalent on the backend ☁️