Backend API application built with Dart Frog, featuring RESTful endpoints, GraphQL support, WebSocket, and real-time communication. Demonstrates CRUD operations, GraphQL queries, middleware implementation, and state management with broadcast_bloc.
Before getting started, make sure you have the following installed:
- Dart SDK: >=3.11.0 <4.0.0
- Dart Frog CLI: Latest version
- IDE: VSCode or IntelliJ IDEA with Dart extensions
If you haven't installed Dart Frog CLI yet, install it globally:
dart pub global activate dart_frog_cliVerify installation:
dart_frog --versiongit clone <repository-url>
cd dart_frog_backend_demodart pub getThis project uses code generation for JSON serialization:
dart run build_runner build --delete-conflicting-outputsStart the Dart Frog development server with hot reload:
dart_frog devThe server will start at http://localhost:8080 by default.
To run on a different port:
dart_frog dev --port 3000The dev server automatically reloads on file changes. No need to restart!
Build an optimized production server:
dart_frog buildThis creates a compiled executable in build/.
dart build/bin/server.dart# Custom port
dart build/bin/server.dart --port 8080
# Custom host
dart build/bin/server.dart --host 0.0.0.0dart_frog_backend_demo/
├── routes/ # API route handlers
│ ├── _middleware.dart # Global middleware
│ ├── index.dart # GET / - Root endpoint
│ ├── ws.dart # WebSocket endpoint
│ ├── graphql.dart # POST /graphql - GraphQL endpoint
│ ├── graphql_ws.dart # WS /graphql_ws - GraphQL subscriptions
│ └── json/ # JSON CRUD endpoints
│ ├── index.dart # GET/POST /json
│ └── [id].dart # GET/PUT/DELETE /json/:id
├── lib/ # Shared business logic
│ ├── graphql/ # GraphQL configuration
│ │ ├── graph_ql_schemas.dart # GraphQL schema (queries, mutations, subscriptions)
│ │ └── graphql_subscriptions.dart # Subscription event streams
│ └── model/ # Data models
│ ├── user/ # User model
│ │ ├── user_model.dart # User data class
│ │ └── schema/ # User GraphQL schema
│ │ └── user_schema.dart # User type definition
│ └── address/ # Address model
│ └── address_model.dart # Address data class
├── test/ # Unit and integration tests
│ ├── routes/ # Route handler tests
│ └── models/ # Model tests
├── public/ # Static files
├── .dart_frog/ # Generated Dart Frog files (don't edit)
├── main.dart # Server entry point
├── pubspec.yaml # Dependencies
└── analysis_options.yaml # Linter rules
Welcome endpoint returning server information.
curl http://localhost:8080/Response:
{
"message": "Welcome to Dart Frog!",
"version": "1.0.0"
}These endpoints demonstrate RESTful operations using the UserModel.
UserModel Structure:
| Field | Type | Description |
|---|---|---|
name |
String |
User's name (required in request) |
age |
Int |
User's age (required in request) |
serverMessage |
String |
Message from server (auto-generated) |
address |
Address |
User's address (auto-generated) |
Get all users.
curl http://localhost:8080/jsonResponse:
[
{
"name": "Dash",
"age": 42,
"serverMessage": "Welcome to Not The Dart Side!",
"address": {
"street": "Bogota",
"number": 2503,
"zipCode": 110721
}
}
]Create a new user.
curl -X POST http://localhost:8080/json \
-H "Content-Type: application/json" \
-d '{"name": "Fabian V.", "age": 30}'Request Body:
{
"name": "Fabian V.",
"age": 30
}Response (201 Created):
{
"message": "ok",
"body": {
"name": "Fabian V.",
"age": 30,
"serverMessage": "Welcome to Not The Dart Side!",
"address": {
"street": "Bogota",
"number": 2503,
"zipCode": 110721
}
}
}Get a specific user by ID.
curl http://localhost:8080/json/1Response:
{
"name": "Dash",
"age": 42,
"serverMessage": "Welcome to Not The Dart Side!",
"address": {
"street": "Bogota",
"number": 2503,
"zipCode": 110721
}
}Update an existing user.
curl -X PUT http://localhost:8080/json/1 \
-H "Content-Type: application/json" \
-d '{"name": "Dash Updated", "age": 43}'Request Body:
{
"name": "Dash Updated",
"age": 43
}Response:
{
"message": "Actualizado el id 1",
"body": {
"name": "Dash Updated",
"age": 43,
"serverMessage": "Welcome to the new The Dart Side!",
"address": {
"street": "Bogota",
"number": 2503,
"zipCode": 110721
}
}
}Delete a user.
curl -X DELETE http://localhost:8080/json/1Response (204 No Content):
(empty response body)
WebSocket endpoint for real-time bidirectional communication.
JavaScript Client Example:
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onopen = () => {
console.log('Connected');
ws.send(JSON.stringify({ type: 'ping' }));
};
ws.onmessage = (event) => {
console.log('Received:', event.data);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('Disconnected');
};Dart Client Example:
import 'package:web_socket_channel/web_socket_channel.dart';
final channel = WebSocketChannel.connect(
Uri.parse('ws://localhost:8080/ws'),
);
channel.stream.listen((message) {
print('Received: $message');
});
channel.sink.add('{"type": "ping"}');GraphQL endpoint for querying data with flexible queries.
Query - Get all users:
curl -X POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ users { name age serverMessage address { street number zipCode } } }"}'Response:
{
"data": {
"users": [
{
"name": "Dash",
"age": 42,
"serverMessage": "Hello from server",
"address": {
"street": "Main St",
"number": 123,
"zipCode": 12345
}
}
]
}
}Query - Find user by ID:
curl -X POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ findUser(id: 1) { name age address { street number } } }"}'Response:
{
"data": {
"findUser": {
"name": "Dash",
"age": 42,
"address": {
"street": "Main St",
"number": 123
}
}
}
}Available Queries:
| Query | Arguments | Returns | Description |
|---|---|---|---|
users |
- | [User] |
Get all users |
findUser |
id: Int |
User |
Find a user by ID |
Available Mutations:
| Mutation | Arguments | Returns | Description |
|---|---|---|---|
createUser |
name: String!, age: Int! |
User |
Create a new user |
updateUser |
id: Int!, name: String, age: Int |
User |
Update an existing user |
deleteUser |
id: Int! |
Boolean |
Delete a user by ID |
Mutation - Create user:
curl -X POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-d '{"query": "mutation { createUser(name: \"John\", age: 25) { name age serverMessage } }"}'Response:
{
"data": {
"createUser": {
"name": "John",
"age": 25,
"serverMessage": "User created successfully!"
}
}
}Mutation - Update user:
curl -X POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-d '{"query": "mutation { updateUser(id: 1, name: \"Jane\", age: 30) { name age serverMessage } }"}'Mutation - Delete user:
curl -X POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-d '{"query": "mutation { deleteUser(id: 1) }"}'Mutation with variables:
curl -X POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation CreateUser($name: String!, $age: Int!) { createUser(name: $name, age: $age) { name age } }",
"variables": { "name": "Alice", "age": 28 }
}'User Type:
| Field | Type | Description |
|---|---|---|
name |
String |
User's name |
age |
Int |
User's age |
serverMessage |
String |
Message from server |
address |
Address |
User's address |
Address Type:
| Field | Type | Description |
|---|---|---|
street |
String |
Street name |
number |
Int |
Street number |
zipCode |
Int |
ZIP code |
WebSocket endpoint for GraphQL subscriptions with real-time updates.
Available Subscriptions:
| Subscription | Arguments | Returns | Description |
|---|---|---|---|
onUserCreated |
- | User |
Emits when a user is created via mutation |
onUserUpdated |
- | User |
Emits when a user is updated via mutation |
onUserDeleted |
- | Int |
Emits user ID when deleted via mutation |
countdown |
from: Int! |
Int |
Countdown from a number (1 per second) |
JavaScript Client Example:
const ws = new WebSocket('ws://localhost:8080/graphql_ws');
// Initialize connection
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'connection_init' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'connection_ack') {
// Subscribe to new users
ws.send(JSON.stringify({
id: '1',
type: 'subscribe',
payload: {
query: `subscription { onUserCreated { name age serverMessage } }`
}
}));
// Subscribe to countdown
ws.send(JSON.stringify({
id: '2',
type: 'subscribe',
payload: {
query: `subscription { countdown(from: 5) }`
}
}));
}
if (data.type === 'next') {
console.log('Received:', data.payload.data);
}
if (data.type === 'complete') {
console.log('Subscription completed:', data.id);
}
};
// Stop a subscription
ws.send(JSON.stringify({ id: '1', type: 'stop' }));Dart Client Example:
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
final channel = WebSocketChannel.connect(
Uri.parse('ws://localhost:8080/graphql_ws'),
);
// Initialize connection
channel.sink.add(jsonEncode({'type': 'connection_init'}));
// Listen for messages
channel.stream.listen((message) {
final data = jsonDecode(message as String) as Map<String, dynamic>;
if (data['type'] == 'connection_ack') {
// Subscribe to user created events
channel.sink.add(jsonEncode({
'id': '1',
'type': 'subscribe',
'payload': {
'query': 'subscription { onUserCreated { name age } }',
},
}));
}
if (data['type'] == 'next') {
print('Received: ${data['payload']}');
}
});Testing with Postman:
-
Create a WebSocket request:
- Click New → WebSocket
- Enter URL:
ws://localhost:8080/graphql_ws - Click Connect
-
Initialize connection:
{"type": "connection_init"}You'll receive:
{"type": "connection_ack"} -
Subscribe to events:
{ "id": "1", "type": "subscribe", "payload": { "query": "subscription { onUserCreated { name age serverMessage } }" } } -
Test the subscription: In another Postman tab, trigger the mutation:
POST http://localhost:8080/graphql Content-Type: application/json {"query": "mutation { createUser(name: \"Test\", age: 25) { name } }"}You'll see the event in the WebSocket connection.
-
Other subscription examples:
// Subscribe to user updates { "id": "2", "type": "subscribe", "payload": { "query": "subscription { onUserUpdated { name age serverMessage } }" } } // Subscribe to countdown { "id": "3", "type": "subscribe", "payload": { "query": "subscription { countdown(from: 10) }" } }
-
Stop a subscription:
{"id": "1", "type": "stop"}
WebSocket Protocol Messages:
| Type | Direction | Description |
|---|---|---|
connection_init |
Client → Server | Initialize WebSocket connection |
connection_ack |
Server → Client | Connection acknowledged |
subscribe |
Client → Server | Start a subscription |
next |
Server → Client | Subscription data event |
complete |
Server → Client | Subscription completed |
stop |
Client → Server | Stop a subscription |
ping |
Client → Server | Heartbeat ping |
pong |
Server → Client | Heartbeat response |
error |
Server → Client | Error message |
- CRUD Operations: Complete Create, Read, Update, Delete functionality
- JSON Serialization: Type-safe JSON handling with code generation
- UUID Generation: Unique identifiers for resources
- HTTP Methods: Support for GET, POST, PUT, DELETE
- Request Validation: Input validation and error handling
- Response Formatting: Consistent JSON response structure
- Flexible Queries: Query exactly the data you need
- Mutations: Create, update, and delete operations
- Subscriptions: Real-time updates via WebSocket
- Type System: Strongly typed schema with User and Address types
- Nested Objects: Support for nested object queries (User -> Address)
- Query Arguments: Support for query parameters (e.g.,
findUser(id: 1)) - Schema Definition: Clean schema definition using
graphql_schema2
- Real-time Communication: Bidirectional communication with clients
- Broadcast Messages: Send messages to all connected clients
- Event Handling: Custom event types and handlers
- Connection Management: Track connected clients
- State Synchronization: Real-time state updates across clients
- Global Middleware: Applied to all routes via
_middleware.dart - Request Logging: Log all incoming requests
- CORS Headers: Cross-Origin Resource Sharing support
- Error Handling: Centralized error handling
- Request Timing: Track request duration
- Custom Headers: Add custom response headers
- broadcast_bloc: Reactive state management for WebSocket
- In-Memory Storage: Fast in-memory data storage (development)
- State Broadcasting: Broadcast state changes to WebSocket clients
- Event Sourcing: Track all state changes
- Hot Reload: Instant server reload on code changes
- Type Safety: Full Dart type safety
- Code Generation: Automated JSON serialization
- Testing Support: Built-in testing utilities
- Linting: Strict code quality with very_good_analysis
The global middleware handles:
- CORS Headers: Enable cross-origin requests
- Request Logging: Log method, path, and timestamp
- Error Handling: Catch and format errors
- Performance Monitoring: Track request duration
Example middleware implementation:
Handler middleware(Handler handler) {
return handler
.use(requestLogger())
.use(provider<String>((_) => 'dependency'))
.use(cors());
}Create route-specific middleware by adding _middleware.dart in any route directory.
dart testdart test --coverage=coverage# Install coverage tools
dart pub global activate coverage
# Generate LCOV report
dart pub global run coverage:format_coverage \
--lcov \
--in=coverage \
--out=coverage/lcov.info \
--report-on=lib
# Generate HTML report
genhtml coverage/lcov.info -o coverage/html
# Open in browser
open coverage/html/index.htmldart test test/routes/json/index_test.dartThe project uses very_good_analysis to maintain code quality:
dart analyzedart format .dart fix --apply- dart_frog: Fast, minimalistic backend framework for Dart
- dart_frog_web_socket: WebSocket support for Dart Frog
- broadcast_bloc: BLoC pattern with broadcast capabilities
- graphql_schema2: GraphQL schema definition
- graphql_server2: GraphQL server implementation
- json_annotation: JSON serialization annotations
- json_serializable: Code generation for JSON serialization
- uuid: Generate unique identifiers
- build_runner: Code generation runner
- test: Dart testing framework
- mocktail: Mocking library for tests
- very_good_analysis: Strict lint rules
Dart Frog uses a file-system based routing:
- Files in
routes/directory automatically become endpoints index.dartmaps to directory path[param].dartcreates dynamic route parameters_middleware.dartapplies middleware to routes in the directory
- Request Received: Incoming HTTP request
- Middleware Chain: Global and route-specific middleware
- Route Handler: Matched route handler executes
- Response: JSON or custom response returned
- Logging: Request logged and metrics collected
Dart Frog supports dependency injection via provider:
// In middleware
handler.use(provider<DatabaseService>((_) => DatabaseService()));
// In route
final database = context.read<DatabaseService>();Create a Dockerfile:
FROM dart:stable AS build
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get
COPY . .
RUN dart pub get --offline
RUN dart run build_runner build --delete-conflicting-outputs
RUN dart_frog build
FROM dart:stable-slim
COPY --from=build /app/build/bin/server /app/bin/
COPY --from=build /app/public /app/public
EXPOSE 8080
CMD ["/app/bin/server"]Build and run:
docker build -t dart-frog-app .
docker run -p 8080:8080 dart-frog-app# Build production server
dart_frog build
# Deploy to Cloud Run
gcloud run deploy dart-frog-app \
--source . \
--platform managed \
--region us-central1 \
--allow-unauthenticatedUse dart_frog_lambda package for AWS Lambda deployment.
Create Procfile:
web: dart build/bin/server.dart --port $PORT
Deploy:
heroku create
git push heroku mainUse environment variables for configuration:
import 'dart:io';
final port = int.parse(Platform.environment['PORT'] ?? '8080');
final host = Platform.environment['HOST'] ?? 'localhost';
final databaseUrl = Platform.environment['DATABASE_URL'];Create .env file (don't commit):
PORT=8080
HOST=localhost
DATABASE_URL=postgresql://localhost/db
API_KEY=your_secret_keyLoad with a package like dotenv:
import 'package:dotenv/dotenv.dart';
final env = DotEnv()..load();
final apiKey = env['API_KEY'];Install Dart Frog CLI globally:
dart pub global activate dart_frog_cliAdd to PATH (if needed):
export PATH="$PATH":"$HOME/.pub-cache/bin"Change the port:
dart_frog dev --port 3000Or kill the process using port 8080:
# macOS/Linux
lsof -ti:8080 | xargs kill -9
# Windows
netstat -ano | findstr :8080
taskkill /PID <PID> /F- Save the file explicitly
- Check for syntax errors
- Restart dev server:
dart_frog dev
- Verify WebSocket endpoint is running
- Check CORS configuration
- Use correct protocol (ws:// not http://)
- Check firewall settings
Clean and regenerate:
dart run build_runner clean
dart pub get
dart run build_runner build --delete-conflicting-outputsEnsure test dependencies are installed:
dart pub getRun with verbose output:
dart test --reporter=expandedAdd CORS middleware in _middleware.dart:
Handler middleware(Handler handler) {
return handler.use(cors());
}
Middleware cors() {
return (handler) {
return (context) async {
final response = await handler(context);
return response.copyWith(
headers: {
...response.headers,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type',
},
);
};
};
}- Run code generation first
- Check for Dart analysis errors
- Verify all dependencies are compatible
dart analyze
dart run build_runner build --delete-conflicting-outputs
dart_frog buildThis project is licensed under the MIT License - see the LICENSE for details.
Generated with Dart Frog 🐸