Search the site
BACKEND
Write a Dart function under lib/backend/functions and it is an HTTP endpoint.
Call it from your app as a typed function, with no client code to write.
ON THIS PAGE
Write a backend function
Call it from the app
Receive parameters
Use DVContext for the session and commit hooks
Serve a webhook at a fixed path
Watch a request move through its stages
Stream results with server-sent events
Undo partial work with a transaction
Move slow work to a job
Protect a function with a policy and MFA
Add middleware
CSRF protection is on for every function
Get an OpenAPI document for free
Status
// lib/backend/functions/hello.get.dart is served at GET /api/hello.
import 'package:dartvel_core/dartvel.dart';
@DVBackendFunction()
Future<Map<String, Object?>> _hello(String name) async =>
<String, Object?>{'greeting': 'Hello, $name'};Copy code to clipboard
The file name sets the method: .get, .post, .put, .patch, .delete, .head or .options. No suffix means POST.
The path comes from the folders, with [id] as a parameter and index as the folder itself.
Every route is served under apiBasePath, /api by default.
Backend files import dartvel_core
The backend runs without Flutter. Import package:dartvel_core/dartvel.dart there, and leave DV and the generated client barrel to the app. The generated data models import Flutter, so a backend function cannot call Order.find() or order.save() yet.
// In a page or anywhere in the app.
final Map<String, Object?> greeting = await hello(name: 'Ada');
final Map<String, Object?> order = await updateOrder(id: '42', status: 'shipped');
getTicks().listen((String tick) => DV.log(tick));Copy code to clipboard
Parameters become required named arguments.
String, int, double, bool, List and Map results decode for you. Other types take a fromJson argument.
The raw call is there too, named after the route: getHello(...) returns the whole response.
Keep the two names apart
The raw call for hello.get.dart is getHello. A function named _getHello in that file would generate the same name, and the build stops and suggests another.
Each parameter is read from the path, then the query string, then the body.
String, int, double and bool values are converted for you.
// lib/backend/functions/orders/[id].put.dart is served at PUT /api/orders/:id.
import 'package:dartvel_core/dartvel.dart';
@DVBackendFunction(
policy: 'Order.update',
mfa: DVMfa.recent(Duration(minutes: 15)),
)
Future<Map<String, Object?>> _updateOrder(
DVContext context, // injected, and never sent by the client
String id,
String status,
) async {
context.afterCommit(() => notifyCustomer(id));
return <String, Object?>{
'id': id,
'status': status,
'by': context.session?.userId,
};
}Copy code to clipboard
A first parameter of type DVContext is injected and left out of the client call.
context.session has the signed-in user, their tenant and claims.
context.afterCommit runs once the function succeeds. context.compensate runs if it fails.
// Served at POST /payments/webhook, outside /api.
@DVBackendFunction(rawPath: '/payments/webhook')
Future<Map<String, Object?>> _paymentWebhook(String event, String reference) async {
await recordPayment(event, reference);
return <String, Object?>{'received': true};
}Copy code to clipboard
rawPath serves the function at exactly that path, outside apiBasePath. rawPathSuffix adds to the generated path instead, and you can use only one of the two.
A raw path reads no session cookie and asks for no CSRF token, so a payment provider can POST to it. An API key or OAuth token sent to it is still checked.
Both take plain segments. A path parameter in one stops the build, and so do two functions on the same address.
@DVBackendFunction()
Future<Map<String, Object?>> _getExport(DVContext context, String id) async {
final DVLifecycleSignal<DVRequestLifecycle> request = context.lifecycle.request;
request.listen((DVRequestLifecycle state) {
if (state == DVRequestLifecycle.failed) {
DV.log('export $id failed', level: DVLogLevel.warn);
}
});
return <String, Object?>{'id': id, 'stage': request.value.name};
}Copy code to clipboard
context.lifecycle.request is read-only. You observe it, and the framework moves it.
Before your code runs, the body is read and the MFA and policy checks pass, with the CSRF check first on a generated path.
An error thrown by the function answers 500, runs its compensations and is recorded as a server crash.
Partial
Spec section: Backend Function Request Lifecycle
Planned work and implementation limits
Four stages are set today: received, executing, preparingResponse and failed.
transaction, authentication and rateLimit are not parameters of @DVBackendFunction yet.
The generated client does not send the binary flat-buffer format, though the server decodes it.
// lib/backend/functions/stream/ticks.get.dart
import 'package:dartvel_core/dartvel.dart';
@DVBackendFunction()
Stream<String> _getTicks() => Stream<String>.periodic(
const Duration(seconds: 1),
(int i) => 'tick $i',
).take(10);Copy code to clipboard
Return a Stream and the endpoint responds with text/event-stream. The client function returns a Stream you listen to.
Built
Spec section: Streaming Functions
@DVBackendFunction()
Future<Map<String, Object?>> _checkout(String orderId) =>
DVTransactionRunner()((DVContext context) async {
final String charge = await chargeCard(orderId);
// Runs if anything later in this transaction throws.
context.compensate(() => refund(charge));
// Runs only once everything succeeded.
context.afterCommit(() => sendReceipt(orderId));
return <String, Object?>{'charge': charge};
});Copy code to clipboard
If the body throws, compensations run in reverse order and the error is rethrown.
In the app, the same call is DV.transaction((DVContext context) async { ... }).
A nested transaction joins the outer one unless you pass isolated: true.
Dispatch a job and return straight away. A worker runs it with retries.
// lib/backend/functions/signup.post.dart
import 'package:dartvel_core/dartvel.dart';
import '../../dartvel_client/jobs.g.dart';
@DVBackendFunction()
Future<Map<String, Object?>> _signup(String userId) async {
// The response goes out now. A worker sends the email.
await SendWelcomeEmail(userId: userId).dispatch();
return <String, Object?>{'queued': true};
}Copy code to clipboard
See Queues and jobs for declaring the job and running workers.
Built
Spec section: Background and Durable Work
Implementation notes
background: and durable: are not parameters of @DVBackendFunction yet. Dispatch a job as shown here.
policy: 'Order.update' asks the OrderPolicy class. A refusal is 403, or 401 when nobody is signed in.
mfa: DVMfa.required, or DVMfa.recent(duration), needs a recent second factor.
A policy nothing answers stops the build.
See Authorization for writing the policy.
@DVUseMiddleware(<DVMiddlewareKey>[
DVMiddlewares.auth,
DVMiddlewares.rateLimit,
])
@DVBackendFunction()
Future<List<String>> _listReports() async => <String>['2026-08', '2026-09'];Copy code to clipboard
Middleware runs in the order you list it. The keys that run are auth, tenant, rateLimit, requestLogging, securityHeaders, locale, idempotency, featureFlags, maintenance and csp.
Every response already carries X-Content-Type-Options, X-Frame-Options (SAMEORIGIN) and a Referrer-Policy, and HSTS when the server terminates TLS. A header your handler sets wins; pass serve() an empty securityHeaders map to send none.
bodyLimit and uploadLimit cap the request body at 1 MiB and 16 MiB, and answer 413 past it. tracing starts a trace for the request.
A key that does nothing yet, such as cors or cacheTags, stops the build and says why.
# pubspec.yaml
dartvel:
server:
maxBodyBytes: 2097152
compression: true
trustedProxies: [10.0.0.0/8]Copy code to clipboard
CORS sits in the same server block. Every key there is checked when dartvel routes runs.
Partial
Spec section: Middleware
Planned work and implementation limits
Page middleware cannot preload data or set SEO context.
No layout, model or storage scopes, and no global middleware setting.
A POST, PUT, PATCH or DELETE to a generated function needs the x-dartvel-csrf-token header, or it gets 403.
The generated client sends the token for you, so there is nothing to add to your app.
Writing your own client? DV.CSRF.token() makes a token and DVCSRF.headerName names the header.
Built
Spec section: CSRF Protection
The backend serves an OpenAPI 3.1 description of your functions at /api/openapi.json.
Built
Spec section: Backend
Implementation notes
Functions are served over HTTP, and a Stream result over server-sent events. dartvel_shelf, the server the backend runs on, has served WebSockets since 0.9.2, but no backend function is given a WebSocket or polling transport yet.
FSL-1.1-MIT licensed. Built with Dartvel.
Dartvel is made by
To the bottom