Server API

Server API#

Overview#

Duxt provides a fullstack development experience. The server/ directory contains your backend code:

server/
├── main.dart           Entry point
├── db.dart             Database connection
├── models/
│   └── post.dart       Data models
└── api/
    └── posts.dart      API routes

Run duxt dev to start both frontend and API server simultaneously.

Quick Start#

The default template includes a complete blog example with SQLite:

duxt create my_app
cd my_app
duxt dev

This starts:

  • Frontend: http://localhost:4000
  • API: http://localhost:3001

Database Setup#

server/db.dart#

import 'package:sqlite3/sqlite3.dart' as sqlite;

class Db {
  static late sqlite.Database _db;

  static void init([String path = 'blog.db']) {
    _db = sqlite.sqlite3.open(path);
    _createTables();
  }

  static void _createTables() {
    _db.execute('''
      CREATE TABLE IF NOT EXISTS posts (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        slug TEXT UNIQUE NOT NULL,
        content TEXT NOT NULL,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP
      )
    ''');
  }

  static sqlite.Database get db => _db;
}

Models#

server/models/post.dart#

import 'package:sqlite3/sqlite3.dart' as sqlite;
import '../db.dart';

class Post {
  final int? id;
  final String title;
  final String slug;
  final String content;

  Post({this.id, required this.title, required this.slug, required this.content});

  factory Post.fromRow(sqlite.Row row) => Post(
    id: row['id'] as int,
    title: row['title'] as String,
    slug: row['slug'] as String,
    content: row['content'] as String,
  );

  factory Post.fromJson(Map<String, dynamic> json) => Post(
    id: json['id'] as int?,
    title: json['title'] as String,
    slug: json['slug'] as String,
    content: json['content'] as String,
  );

  Map<String, dynamic> toJson() => {
    'id': id, 'title': title, 'slug': slug, 'content': content,
  };

  // Repository methods
  static List<Post> findAll() {
    return Db.db.select('SELECT * FROM posts ORDER BY created_at DESC')
        .map(Post.fromRow).toList();
  }

  static Post? findBySlug(String slug) {
    final r = Db.db.select('SELECT * FROM posts WHERE slug = ?', [slug]);
    return r.isEmpty ? null : Post.fromRow(r.first);
  }

  static Post create(Post post) {
    Db.db.execute(
      'INSERT INTO posts (title, slug, content) VALUES (?, ?, ?)',
      [post.title, post.slug, post.content],
    );
    return findById(Db.db.lastInsertRowId)!;
  }

  static Post? findById(int id) {
    final r = Db.db.select('SELECT * FROM posts WHERE id = ?', [id]);
    return r.isEmpty ? null : Post.fromRow(r.first);
  }
}

API Routes#

server/api/posts.dart#

import 'package:duxt/server.dart';
import '../models/post.dart';

void registerPostRoutes(DuxtServer server) {
  // GET /api/posts
  server.get('/api/posts', (req) {
    final posts = Post.findAll();
    return json({'posts': posts.map((p) => p.toJson()).toList()});
  });

  // GET /api/posts/:slug
  server.get('/api/posts/:slug', (req) {
    final slug = req.params['slug'];
    final post = Post.findBySlug(slug!);
    if (post == null) return json({'error': 'Not found'}, statusCode: 404);
    return json({'post': post.toJson()});
  });

  // POST /api/posts
  server.post('/api/posts', (req) {
    final body = req.body as Map<String, dynamic>;
    final post = Post.create(Post.fromJson(body));
    return json({'post': post.toJson()}, statusCode: 201);
  });
}

Server Entry Point#

server/main.dart#

import 'package:duxt/server.dart';
import 'db.dart';
import 'models/post.dart';
import 'api/posts.dart';

void main() {
  // Initialize database
  Db.init();

  // Create server with middleware
  final server = DuxtServer(
    port: 3001,
    middleware: [securityHeaders(), bodyLimit(), cors(origins: ['http://localhost:4000']), jsonBody(), logger()],
  );

  // Register routes
  registerPostRoutes(server);

  // Start
  server.start();
}

DuxtServer API#

Creating a Server#

final server = DuxtServer(
  port: 3001,
  middleware: [securityHeaders(), bodyLimit(), cors(origins: ['http://localhost:4000']), jsonBody(), logger()],
);

Route Methods#

server.get('/path', handler);
server.post('/path', handler);
server.put('/path', handler);
server.delete('/path', handler);
server.patch('/path', handler);

Route Parameters#

server.get('/posts/:id', (req) {
  final id = req.params['id'];  // Access route params
  final page = req.query('page'); // Access query params
  final body = req.body;          // Access JSON body
  // ...
});

Response Helpers#

return json({'data': data});                    // 200 OK
return json({'data': data}, statusCode: 201);   // 201 Created
return json({'error': 'Not found'}, statusCode: 404);

Built-in Middleware#

Security Headers#

securityHeaders()  // X-Content-Type-Options, X-Frame-Options, X-XSS-Protection

Body Limit#

bodyLimit()                         // Default 1 MB limit
bodyLimit(maxBytes: 5 * 1024 * 1024) // Custom 5 MB limit

Rate Limiting#

rateLimit()                         // 100 requests/min per IP
rateLimit(maxRequests: 50, window: Duration(seconds: 30))

CORS#

Requires explicit origins. Use ['*'] only for public APIs:

cors(origins: ['https://myapp.com'])
cors(origins: ['*'])  // Development only

JSON Body Parser#

jsonBody()  // Parses JSON request bodies

Logger#

logger()  // Logs requests: GET /api/posts 200 12ms

Auth#

auth((request) async {
  final token = request.headers['authorization'];
  return token != null && await verifyToken(token);
})

See the Security guide for the complete middleware reference.

Calling the API from Frontend#

import 'dart:convert';
import 'dart:js_interop';
import 'package:web/web.dart' as web;

Future<List<Post>> fetchPosts() async {
  final response = await web.window.fetch('http://localhost:3001/api/posts'.toJS).toDart;
  final jsText = await response.text().toDart;
  final data = jsonDecode(jsText.toDart);
  return (data['posts'] as List).map((p) => Post.fromJson(p)).toList();
}

Development#

# Start both frontend and API
duxt dev

# Options
duxt dev --port=4000        # Frontend port
duxt dev --api-port=3001    # API port
duxt dev --no-api           # Skip API server

Production Build#

# Build for current platform
duxt build

# Build for specific target
duxt build --target=linux-x64
duxt build --target=linux-arm64
duxt build --target=macos-x64
duxt build --target=macos-arm64

# Build for all targets (uses Docker for cross-compilation)
duxt build --all-targets

Output Structure#

.output/
├── public/              Static frontend files
└── server-<target>      Server binary (e.g., server-linux-x64)

Deployment#

Frontend: Deploy public/ to any static hosting (Vercel, Netlify, Cloudflare Pages, nginx).

Server: Run the binary on your server:

# On your server
./.output/server-linux-x64

Cross-Compilation#

TargetNativeDocker
linux-x64✓ (on Linux)✓
linux-arm64✓ (on ARM Linux)✓
macos-x64✓ (on Intel Mac)✗
macos-arm64✓ (on Apple Silicon)✗

Linux targets can be cross-compiled using Docker. macOS targets must be built on macOS.