Flint Dart
A modern, production‑ready backend framework for Dart. Flint Dart gives you routing, middleware, ORM, authentication, validation, views, and auto‑generated Swagger docs—built for real apps, not just demos.
- Website: flintdart.eulogia.net
- Status: Internal Build (v1.0.0+29)
- Maintainers: Eulogia Technologies
Why Flint Dart
- Fast, clean routing for REST APIs
- Middleware-first design for security and control
- Built‑in ORM for MySQL and PostgreSQL
- Auth helpers (JWT + social providers)
- Validation on requests (JSON + form)
- Views with a simple template engine
- Swagger UI auto‑docs from route annotations
- CLI to scaffold projects and files
Installation
1) Install CLI globally
# Activate CLI
dart pub global activate flint_dart
# Create a new project
flint create my_app
cd my_app
# Run the app
flint run
2) Add to an existing project
dart pub add flint_dart
Hello World
import 'package:flint_dart/flint_dart.dart';
void main() {
final app = Flint();
app.get('/', (Context ctx) async {
return ctx.res?.send('Welcome to Flint Dart!');
});
app.listen(port: 3000);
}
Routing
app.get('/users/:id', (Context ctx) async {
final id = ctx.req.param('id');
return ctx.res?.json({'id': id});
});
app.post('/users', (Context ctx) async {
final body = await ctx.req.json();
return ctx.res?.json({'created': true, 'data': body});
});
Controller-based routes (v1.0.0+29)
Flint also supports request-scoped controllers for both HTTP and WebSocket routes.
Controller methods can use req, res, and socket directly after binding.
import 'package:flint_dart/flint_dart.dart';
class UserController extends Controller {
Future<Response> create() async {
final body = await req.json();
return res.json({'created': true, 'data': body});
}
}
class ChatController extends Controller {
void connect() {
socket.emit('connected', {'id': socket.id});
}
}
Route registration:
app.post(
'/users',
controllerAction(UserController.new, (c) => c.create()),
);
app.websocket(
'/chat',
controllerAction(ChatController.new, (c) {
c.connect();
return null;
}),
);
RouteGroup shorthand (no mixin required):
class UserRoutes extends RouteGroup {
@override
String get prefix => '/users';
@override
void register(Flint app) {
app.post('/', useController(UserController.new, (c) => c.create()));
}
}
Unified Context handlers
Flint uses a unified Context object for route handlers.
ctx.reqis always available.ctx.resis available for HTTP routes.ctx.socketis available for WebSocket routes.ctx.extrasandctx.read<T>()/ctx.write<T>()support future request-scoped injection (e.g. session/user).
app.get('/health', (Context ctx) {
return ctx.res?.json({'ok': true});
});
app.websocket('/chat', (Context ctx) {
ctx.socket?.on('ping', (_) {
ctx.socket?.emit('pong', {'ok': true});
});
});
Request helpers
ctx.req.param('id')— route parameterctx.req.queryParam('page')— query parameterctx.req.body()— raw body stringctx.req.json()— JSON body (Map)ctx.req.form()— form data (Map)
Response helpers
ctx.res?.send('text')ctx.res?.json({...})ctx.res?.view('home', data: {...})ctx.res?.respond(data)— auto‑detects typectx.res?.back(fallback: '/settings')— redirect to previous URLctx.res?.withSuccess('Saved')/ctx.res?.withError('Failed')— flash messages for next template render
You can also return data directly from a handler. Flint will serialize:
Map/Listas JSON- primitives as text/auto response
- custom objects that implement
toMap()ortoJson()
app.post('/settings', (Context ctx) async {
final data = await ctx.req.validate({'name': 'required|string|min:2'});
// ... persist data
return ctx.res
?.withSuccess('Settings updated successfully.')
.back(fallback: '/settings');
});
class UserDto {
final int id;
final String email;
UserDto(this.id, this.email);
Map<String, dynamic> toMap() => {'id': id, 'email': email};
}
app.get('/me', (Context ctx) async {
return UserDto(1, 'ada@example.com'); // auto JSON via toMap()
});
Middleware
Global middleware
app.use(AuthMiddleware());
Route middleware
app.get('/admin', handler).use(AuthMiddleware());
Route group middleware
class AdminRoutes extends RouteGroup {
@override
String get prefix => '/admin';
@override
List<Middleware> get middlewares => [AuthMiddleware()];
@override
void register(Flint app) {
app.get('/users', (Context ctx) async => ctx.res?.json([]));
}
}
Built‑in middleware:
ExceptionMiddleware— error handlingCookieSessionMiddleware— cookies/sessionsCorsMiddleware— CORS headersLoggerMiddleware— request loggingStaticFileMiddleware— static files
Validation
app.post('/register', (Context ctx) async {
final data = await ctx.req.validate({
'name': 'required|string|min:3',
'email': 'required|email',
'password': 'required|string|min:8',
}, messages: {
'email.required': 'Email is required.',
'password.min': 'Password must be at least :min characters.',
});
return ctx.res?.json({'ok': true, 'data': data});
});
Views & Templates
Flint’s view engine uses .flint.html templates with a small set of processors. Views live in
lib/views and can extend layouts, include partials, loop, and render variables.
Render a view
app.get('/', (Context ctx) async {
return ctx.res?.view('home', data: {'title': 'Flint Docs'});
});
Layouts, sections, and yield
<!-- lib/views/layouts/app.flint.html -->
<!doctype html>
<html>
<head>
<title>{{ title ?? 'Flint' }}</title>
</head>
<body>
{{ yield('content') }}
</body>
</html>
<!-- lib/views/home.flint.html -->
{{ extends('layouts.app') }}
{{ section('content') }}
<h1>{{ title }}</h1>
{{ endsection }}
Includes (partials)
{{ include('partials.nav') }}
Variables
<h2>{{ user.name }}</h2>
Conditionals
{{ if user }}
<p>Welcome back, {{ user.name }}</p>
{{ endif }}
Loops
<ul>
{{ for item in items }}
<li>{{ item }}</li>
{{ endfor }}
</ul>
Switch / Case
{{ switch status }}
{{ case 'paid' }}<span>Paid</span>{{ endcase }}
{{ case 'pending' }}<span>Pending</span>{{ endcase }}
{{ default }}<span>Unknown</span>{{ enddefault }}
{{ endswitch }}
Built‑in Processors
extends— layout inheritancesection/yield— slot content into layoutsinclude— partials/partials with datavariables—{{ ... }}interpolationif_statement—if/endiffor_loop—for/endforswitch_cases—switch/case/defaultcomment— template commentsassets— asset helper tagssession— session/error helpers in templatesold_processor— legacy tags support
Mail (SMTP + Flint Templates)
Flint supports sending email with SMTP and rendering email bodies from .flint.html templates.
Auto connect mail on app start
Mail config is loaded automatically when the server starts:
final app = Flint(); // autoConnectMail is true by default
await app.listen(port: 3000);
If you want full manual control:
final app = Flint(autoConnectMail: false);
await MailConfig.load(); // manual setup from .env
await app.listen(port: 3000);
.env mail config
MAIL_PROVIDER=custom # custom | gmail | outlook | yahoo
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your_username
MAIL_PASSWORD=your_password
MAIL_ENCRYPTION=tls # tls | ssl
MAIL_FROM_ADDRESS=noreply@example.com
MAIL_FROM_NAME=My App
Send mail directly
await Mail()
.to('user@example.com')
.subject('Welcome')
.html('<h1>Hello</h1>')
.text('Hello')
.sendMail();
Text-only mail:
await Mail()
.to('user@example.com')
.subject('Plain Text Message')
.text('Hello from Flint Mail')
.sendMail();
Use Flint view templates for mail (ViewMailable)
Generate a mail class + template:
flint make:mail welcome
This creates:
lib/mail/welcome_mail.dartlib/mail/views/welcome.flint.html
Example:
import 'package:flint_dart/mail.dart';
class WelcomeMail extends ViewMailable {
final String recipientName;
final String recipientEmail;
WelcomeMail({
required this.recipientName,
required this.recipientEmail,
});
@override
String get subject => 'Welcome';
@override
String get view => 'mail/views/welcome.flint.html';
@override
Map<String, dynamic> get data => {
'recipientName': recipientName,
};
@override
List<String> get to => [recipientEmail];
}
await WelcomeMail(
recipientName: 'Ada',
recipientEmail: 'ada@example.com',
).send();
Template rendering notes
- For web pages, use
ctx.res?.view('home', data: {...})with templates inlib/views. - For mail templates,
ViewMailablerenders your.flint.htmlfile viaTemplateEngine().render(...). - If you are looking for old names like
viewFlintTemplateorViewable, use the current APIs above (view,TemplateEngine,ViewMailable).
ORM (CRUD)
// READ
final user = await User().find(1);
final users = await User()
.where('email', 'test@example.com')
.orderBy('created_at', desc: true)
.limit(10)
.get();
// CREATE
final created = await User().create({
'name': 'Ada',
'email': 'ada@example.com',
'password': 'secret',
});
// UPDATE
await User()
.where('id', 1)
.update(data: {'name': 'Ada Lovelace'});
// DELETE
await User().delete(1);
Extra ORM helpers
save()— create/update based on PKfirstOrCreate(where, data)upsert(where, data)/upsertMany([...])countAll()/countWhere(field, value)
Models & Tables
class User extends Model<User> {
User() : super(() => User());
String get name => getAttribute('name');
String get email => getAttribute('email');
@override
Table get table => Table(
name: 'users',
columns: [
Column(name: 'name', type: ColumnType.string, length: 255),
Column(name: 'email', type: ColumnType.string, length: 255),
],
);
}
Relations
class User extends Model<User> {
@override
Map<String, RelationDefinition> get relations => {
'posts': Relations.hasMany('posts', () => Post()),
};
}
class Post extends Model<Post> {
@override
Map<String, RelationDefinition> get relations => {
'author': Relations.belongsTo('author', () => User()),
};
}
final posts = await Post().withRelation('author').get();
Auth (JWT + Providers)
Register / Login
final user = await Auth.register(
email: data['email'],
password: data['password'],
name: data['name'],
additionalData: {'role': 'admin'},
);
final result = await Auth.login(data['email'], data['password']);
Access + Refresh Tokens (Optional)
Refresh token support is opt-in and disabled by default to keep framework behavior flexible.
final tokens = await Auth.loginWithTokens(
data['email'],
data['password'],
throttleKey: req.ipAddress, // optional
ipAddress: req.ipAddress, // optional metadata
userAgent: req.headers['user-agent'],
deviceName: 'web',
);
final rotated = await Auth.refreshAccessToken(
tokens['refreshToken'],
rotateRefreshToken: true,
);
Revoke helpers:
await Auth.revokeRefreshToken(refreshToken);
await Auth.revokeAllRefreshTokensForUser(userId);
Login Throttle (Optional)
Login throttle/lockout is also opt-in and disabled by default.
final result = await Auth.login(
data['email'],
data['password'],
throttleKey: req.ipAddress, // optional (ip/device/etc)
);
OAuth Providers
final url = Auth.providerRedirectUrl(
provider: 'google',
redirectPath: '/auth/google/callback',
);
Supported providers: Google, GitHub, Facebook, Apple.
Auth Environment Options
# Existing
JWT_SECRET=change-me-to-a-strong-secret
JWT_EXPIRY_HOURS=24
PASSWORD_MIN_LENGTH=6
# Optional refresh-token flow
AUTH_ENABLE_REFRESH_TOKENS=false
AUTH_ACCESS_TOKEN_MINUTES=60
AUTH_REFRESH_TOKEN_DAYS=30
# Optional login throttle
AUTH_ENABLE_LOGIN_THROTTLE=false
AUTH_LOGIN_MAX_ATTEMPTS=5
AUTH_LOGIN_LOCK_MINUTES=15
File Uploads & Storage
final upload = await ctx.req.file('avatar');
if (upload != null) {
final path = await ctx.req.storeFile('avatar', directory: 'public/uploads');
}
Swagger UI (Auto Docs)
Enable docs and visit:
http://localhost:3000/docs
Flint parses annotations above your routes:
/// @summary Create a new user
/// @auth bearer
/// @response 201 Created
/// @query page integer optional Page
/// @body {"name": "string", "email": "string"}
app.post('/users', controller.create);
Database
Configure .env:
DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_NAME=flint
DB_USER=root
DB_PASSWORD=secret
DB_SECURE=false
Auto‑connect runs on server start. You can disable it and call DB.connect() manually.
final app = Flint(autoConnectDb: false);
try {
await DB.connect(
database: env('DB_NAME', 'app_db'),
host: env('DB_HOST', 'localhost'),
port: env('DB_PORT', 3306),
username: env('DB_USER', 'root'),
password: env('DB_PASSWORD', ''),
);
} catch (e) {
Log.debug('Database connection failed: $e');
}
Logging
By default, Flint logs to console and does not create log files.
Use .env to control logging:
LOG_ENABLED=true
LOG_TO_CONSOLE=true
LOG_TO_FILE=false
LOG_DIR=logs
LOG_LEVEL=debug
LOG_TO_FILE=falseprevents creating log files on user systems.- Set
LOG_TO_FILE=trueonly when you want persisted log files.
CLI
flint create my_app
flint run
flint build
flint docs:generate
flint make:model User
flint make:controller UserController
flint make:middleware AuthMiddleware
Contributing
Contributions are welcome. Open issues, suggest improvements, or submit PRs.
License
MIT