flint_dart 1.0.0+33 copy "flint_dart: ^1.0.0+33" to clipboard
flint_dart: ^1.0.0+33 copied to clipboard

A modern, expressive, and extensible server-side framework by Eulogia Technologies.

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.req is always available.
  • ctx.res is available for HTTP routes.
  • ctx.socket is available for WebSocket routes.
  • ctx.extras and ctx.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 parameter
  • ctx.req.queryParam('page') — query parameter
  • ctx.req.body() — raw body string
  • ctx.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 type
  • ctx.res?.back(fallback: '/settings') — redirect to previous URL
  • ctx.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 / List as JSON
  • primitives as text/auto response
  • custom objects that implement toMap() or toJson()
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 handling
  • CookieSessionMiddleware — cookies/sessions
  • CorsMiddleware — CORS headers
  • LoggerMiddleware — request logging
  • StaticFileMiddleware — 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 inheritance
  • section / yield — slot content into layouts
  • include — partials/partials with data
  • variables{{ ... }} interpolation
  • if_statementif/endif
  • for_loopfor/endfor
  • switch_casesswitch/case/default
  • comment — template comments
  • assets — asset helper tags
  • session — session/error helpers in templates
  • old_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.dart
  • lib/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 in lib/views.
  • For mail templates, ViewMailable renders your .flint.html file via TemplateEngine().render(...).
  • If you are looking for old names like viewFlintTemplate or Viewable, 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 PK
  • firstOrCreate(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');
}

AI Runtime #

Flint now exposes one AI system only: ai.

The AI layer is organized around:

  • providers
  • tools
  • workflows
  • memory
  • runtime

Framework entry points #

final app = Flint();

app.ai.registerChatProvider(OpenAiChatProvider(apiKey: 'openai-key'));

app.get('/ai/chat', (Context ctx) async {
  final result = await ctx.ai.chat(
    providerId: 'openai',
    request: const ChatRequest(
      model: 'gpt-4o-mini',
      messages: [
        ChatMessage(role: 'user', content: 'Say hello'),
      ],
    ),
  );

  return ctx.res?.json(result.toMap());
});

You can also opt into stricter production defaults:

final ai = FlintAi.production(
  memoryStore: AutoAiMemoryStore(),
  repository: AutoAiRepository(),
);

Register providers, tools, workflows, memory, and persistence #

final app = Flint();

app.ai.registerChatProvider(OpenAiChatProvider(apiKey: 'openai-key'));
app.ai.registerTool(SummarizeTicketTool());
app.ai.registerWorkflow(SupportEscalationWorkflow());

// Optional explicit production wiring
final hardenedAi = FlintAi.production(
  memoryStore: AutoAiMemoryStore(),
  repository: AutoAiRepository(),
);

Runtime model #

  • AiAgent defines planning and synthesis
  • AiTool performs controlled side effects
  • AiWorkflow handles higher-level orchestration
  • AiMemoryStore tracks run memory
  • AiRepository persists runs, traces, threads, and artifacts

Memory and persistence APIs #

await ctx.ai.saveThreadMessage('thread-42', {
  'role': 'user',
  'content': 'Customer cannot reset password',
});

final thread = await ctx.ai.loadThreadMessages('thread-42');
final events = await ctx.ai.loadRunEvents('run-id');

Production note #

Flint uses auto-configured AI memory and repository stores by default:

  • when the database is connected, DB-backed stores are used
  • when it is not connected, Flint falls back to in-memory stores with warnings

For production, connect the database or provide explicit shared stores so runs, traces, threads, and memory survive restarts and work across workers.

Built-in chat providers #

  • OpenAiChatProvider
  • AnthropicChatProvider
  • GeminiChatProvider

These live under ai and use the shared AI provider abstractions. There is no separate public agent namespace anymore.

End-to-end example #

class SummarizeTicketTool extends AiTool {
  @override
  String get name => 'support.summarize';

  @override
  String get description => 'Formats a support summary.';

  @override
  bool get enabledByDefault => true;

  @override
  Future<Map<String, dynamic>> execute(AiToolContext context) async {
    final issue = context.arguments['issue']?.toString() ?? '';
    return {'summary': 'Customer issue: $issue'};
  }
}

class SupportAgent extends AiAgent {
  @override
  String get name => 'support_agent';

  @override
  Future<AiPlan> plan(AiRunContext context) async {
    return AiPlan(
      steps: [
        AiPlanStep(
          id: 'summarize',
          type: 'tool',
          description: 'Create a support summary',
          toolName: 'support.summarize',
          arguments: {'issue': context.goal.input['issue']},
        ),
      ],
    );
  }

  @override
  Future<Map<String, dynamic>> synthesize(AiRunContext context) async {
    return {
      'summary': (context.state['summarize'] as Map)['summary'],
      'events': context.run.events.map((event) => event.toMap()).toList(),
    };
  }
}

final app = Flint();
app.ai.registerTool(SummarizeTicketTool());

app.post('/ai/support', (Context ctx) async {
  final body = await ctx.req.json();
  final threadId = body['threadId']?.toString() ?? 'support-thread';

  await ctx.ai.saveThreadMessage(threadId, {
    'role': 'user',
    'content': body['issue'],
  });

  final run = await ctx.ai.run(
    agent: SupportAgent(),
    goal: AiGoal(
      task: 'Resolve support request',
      input: {'issue': body['issue']},
    ),
    userId: 'support-user',
    threadId: threadId,
    context: ctx,
  );

  return ctx.res?.json({
    'run': run.toMap(),
    'thread': await ctx.ai.loadThreadMessages(threadId),
    'events': await ctx.ai.loadRunEvents(run.run.id),
  });
});

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=false prevents creating log files on user systems.
  • Set LOG_TO_FILE=true only 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

10
likes
150
points
22
downloads

Documentation

API reference

Publisher

verified publisherflintdart.eulogia.net

Weekly Downloads

A modern, expressive, and extensible server-side framework by Eulogia Technologies.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

args, bcrypt, crypto, dart_jsonwebtoken, flint_client, mailer, mime, mysql_dart, package_config, path, postgres, universal_web, uuid, watcher, worker_manager

More

Packages that depend on flint_dart