فلٹر انجینئر کے طور پر، آپ ڈارٹ کو پہلے ہی جانتے ہیں۔ async/انتظار کو سمجھیں، ماڈلز اور ریپوزٹریز کے ساتھ کام کریں، صاف آرکیٹیکچرز میں سوچیں، اور حقیقی ایپلی کیشنز بھیجیں۔
آپ آج کہاں ہیں اور جہاں آپ پروڈکشن بیک اینڈ کی تعمیر اور تعیناتی کر سکتے ہیں اس کے درمیان فرق آپ کے خیال سے کم ہے۔
جو غائب ہے وہ نئی زبان نہیں ہے۔ یہ کوئی نیا نمونہ نہیں ہے۔ یہ سمجھنا کہ ڈارٹ ویجیٹ ٹری، BuildContext، یا Flutter فریم ورک کے بغیر کیسے کام کرتا ہے۔ بس ایک چل رہا عمل ہے جو HTTP درخواستوں کو ہینڈل کرتا ہے، ڈیٹا بیس کے ساتھ بات چیت کرتا ہے، اور کلائنٹ کو جوابات واپس بھیجتا ہے۔
اس مضمون کا احاطہ کرتا ہے۔
ہم شروع سے مکمل صارف اور پروفائل مینجمنٹ REST API بنانے کے لیے Dart اور Shelf کا استعمال کریں گے، Docker پر چلنے والے PostgreSQL ڈیٹا بیس سے جڑیں گے، JWT تصدیق کے ساتھ محفوظ کریں گے، اور Fly.io پر تعینات کریں گے۔
آخر میں، آپ کے پاس کام کرنے والا، پروڈکشن گریڈ بیک اینڈ مکمل طور پر ڈارٹ میں لکھا ہوا ہوگا، وہی زبان جو آپ پہلے سے جانتے ہیں۔
یہ مضمون تین مختلف فریم ورک کا استعمال کرتے ہوئے ایک ہی پروجیکٹ کی تعمیر کے بارے میں ایک سیریز (اسٹینڈ اکیلے مضامین) کا حصہ ہے۔ ہم یہاں شیلف، اگلے مضمون میں Serverpod، اور اس کے بعد آرٹیکل میں Dart Frog استعمال کریں گے۔ یہ آپ کو براہ راست موازنہ کرنے کی اجازت دیتا ہے کہ ہر فریم ورک ایک ہی مسئلے تک کیسے پہنچتا ہے۔
انڈیکس
شرطیں
شروع کرنے سے پہلے، آپ کے پاس ہونا ضروری ہے:
-
ڈارٹ اور فلٹر کی نشوونما کے ساتھ آرام دہ واقفیت
-
REST API کے تصورات، اختتامی نکات، HTTP طریقوں اور اسٹیٹس کوڈز کو سمجھنا۔
-
ڈوکر ڈیسک ٹاپ انسٹال اور چل رہا ہے۔
-
Fly.io اکاؤنٹ (مفت درجے fly.io کے لیے کافی ہے)
-
فلائی سی ایل آئی انسٹال کریں (میک او ایس پر فلائی سی ٹی ایل اور ونڈوز/لینکس پر آفیشل انسٹالر بنائیں)
-
ڈیٹا بیس کی جانچ کے لیے ایک PostgreSQL کلائنٹ جیسے TablePlus یا DBeaver – دونوں اچھی طرح سے کام کرتے ہیں۔
ڈارٹ سرورز پر کیسے کام کرتا ہے۔
جب آپ فلٹر ایپ چلاتے ہیں تو فلٹر فریم ورک بہت زیادہ کام کرتا ہے، جس میں ویجیٹ ٹری کا انتظام کرنا، رینڈر پائپ لائن پر کارروائی کرنا، حالت کو ایڈجسٹ کرنا، اور پلیٹ فارم ایونٹس کا جواب دینا شامل ہے۔ آپ کا ڈارٹ کوڈ ان سب کے اوپر بیٹھا ہے۔
سرور پر کچھ بھی موجود نہیں ہے۔ کوئی ویجیٹ درخت نہیں ہے۔ UI لائف سائیکل کو منظم کرنے کے لیے کوئی فریم ورک نہیں ہے۔ آپ کو صرف یہ کرنا ہے کہ ڈارٹ پراسیس چلائیں، پورٹ پر سنیں، HTTP درخواستیں سنیں، کچھ کام کریں، اور جواب بھیجیں۔
ڈارٹ کی معیاری لائبریری، ڈارٹ: io، میں وہ سب کچھ ہے جس کی آپ کو نچلی سطح پر کرنے کی ضرورت ہے۔
import 'dart:io';
void main() async {
final server = await HttpServer.bind('0.0.0.0', 8080);
print('Server running on port 8080');
await for (final request in server) {
request.response
..statusCode = 200
..write('Hello from Dart')
..close();
}
}
یہ ایک HTTP سرور ہے جو مقامی ڈارٹ پر چلتا ہے۔ کوئی پیکج نہیں، کوئی فریم ورک نہیں ہے۔ تمام درخواستیں HttpServer سٹریم کے ذریعے آتی ہیں اور براہ راست جواب میں لکھتی ہیں۔
یہ کام کرتا ہے، لیکن اچھی طرح سے پیمانہ نہیں ہے۔ جس لمحے آپ کو روٹنگ، مڈل ویئر، توثیق، اور ساختی غلطی سے نمٹنے کی ضرورت ہوتی ہے، raw dart:io غیر منظم ہو جاتا ہے۔ یہ وہ مسئلہ ہے جسے شیلف حل کرتا ہے۔
لیتھ کیا ہے؟
شیلف ایک کمپوز ایبل ویب سرور مڈل ویئر لائبریری ہے جو ڈارٹ کے لیے ڈارٹ ٹیم کے زیر انتظام ہے۔ یہ ایک مکمل فریم ورک بننے کی کوشش نہیں کرتا۔ اس کے بجائے، یہ بلڈنگ بلاکس فراہم کرتا ہے جس سے آپ ایک فریم ورک بنا سکتے ہیں، یا بالکل وہی جو آپ کو درکار ہے۔
شیلف ذہنی ماڈل چار تصورات پر بنایا گیا ہے:
-
مینیجر: یہ ایک ایسا فنکشن ہے جو درخواست وصول کرتا ہے اور جواب واپس کرتا ہے۔ شیلف پر موجود ہر چیز بالآخر ایک ہینڈلر ہے۔
-
مڈل ویئر: ایک فنکشن جو عمل درآمد سے پہلے یا بعد میں رویے کو شامل کرنے کے لیے ہینڈلر کو لپیٹتا ہے۔ لاگنگ، توثیق، اور ایرر ہینڈلنگ سبھی مڈل ویئر ہیں۔
-
پائپ لائن: یہ ایک مڈل ویئر چین ہے جس کے آخر میں ایک ہینڈلر ہوتا ہے۔ درخواستیں ہینڈلر تک پہنچنے سے پہلے ایک مڈل ویئر چین سے گزرتی ہیں۔
-
راؤٹر: مخصوص ہینڈلرز کے لیے URL پیٹرن اور HTTP طریقوں کا نقشہ بنائیں۔
اگر آپ نے کبھی فلٹر کے نیویگیٹر یا فراہم کنندہ مڈل ویئر کے تصورات کا استعمال کیا ہے، تو کمپوزیشن ماڈل واقف محسوس ہوگا۔ چھوٹے، واحد ذمہ داری کے ٹکڑے ایک مجموعی کام کو انجام دینے کے لیے اکٹھے ہوتے ہیں۔
پروجیکٹ کی ترتیبات
ایک پروجیکٹ بنائیں
ڈارٹ میں سرور سائیڈ پروجیکٹ ٹیمپلیٹس شامل ہیں جو ایک صاف نقطہ آغاز فراہم کرتے ہیں۔
dart create -t server-shelf user_profile_api
cd user_profile_api
pubspec.yaml میں مطلوبہ انحصار شامل کریں۔
name: user_profile_api
description: User and Profile Management REST API built with Dart and Shelf
version: 1.0.0
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
shelf: ^1.4.1
shelf_router: ^1.1.4
postgres: ^3.3.0
dart_jsonwebtoken: ^2.12.0
bcrypt: ^1.1.3
dotenv: ^4.1.0
crypto: ^3.0.3
dev_dependencies:
lints: ^3.0.0
test: ^1.24.0
چلائیں:
dart pub get
منصوبے کی ساخت
اب ہم ایک بیک اینڈ پروجیکٹ ڈھانچہ بنائیں گے جسے فلٹر انجینئرز بدیہی طور پر تلاش کر سکتے ہیں، فوری طور پر تشریف لے جانے کے لیے کافی واقف ہے، اور بیک اینڈ کنونشنز کے لیے کافی درست ہے۔
user_profile_api/
bin/
server.dart ← entry point
lib/
config/
database.dart ← connection manager
env.dart ← environment config
handlers/
auth_handler.dart ← auth endpoints
user_handler.dart ← user endpoints
profile_handler.dart ← profile endpoints
middleware/
auth_middleware.dart ← JWT validation
error_middleware.dart ← global error handling
logger_middleware.dart ← request logging
models/
user.dart
profile.dart
repositories/
user_repository.dart
profile_repository.dart
services/
auth_service.dart ← JWT + password logic
router.dart ← route definitions
migrations/
001_create_users.sql
002_create_profiles.sql
docker-compose.yml
Dockerfile
.env
.env.example
خدشات کی یہ علیحدگی اس بات سے براہ راست تعلق رکھتی ہے جو کسی بھی فلٹر انجینئر کو پہلے سے معلوم ہوگا۔ دوسرے الفاظ میں، ماڈلز، ریپوزٹریز، اور سروسز ایک ہی تصور ہیں۔ ایک ہینڈلر ویو ماڈل یا کنٹرولر کی جگہ لے لیتا ہے۔ مڈل ویئر انٹرسیپٹرز کی جگہ لے لیتا ہے۔
ڈوکر کا استعمال کرتے ہوئے ڈیٹا بیس سیٹ اپ
پروجیکٹ روٹ میں docker-compose.yml بنائیں۔
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: user_profile_db
environment:
POSTGRES_DB: user_profile_api
POSTGRES_USER: dart_user
POSTGRES_PASSWORD: dart_password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
ڈیٹا بیس شروع کریں۔
docker compose up -d
یقینی بنائیں کہ یہ چل رہا ہے۔
docker compose ps
# user_profile_db running 0.0.0.0:5432->5432/tcp
ماحولیات کی ترتیب
پروجیکٹ روٹ میں ایک .env بنائیں۔
DB_HOST=localhost
DB_PORT=5432
DB_NAME=user_profile_api
DB_USER=dart_user
DB_PASSWORD=dart_password
JWT_SECRET=your_super_secret_key_change_this_in_production
JWT_EXPIRY_HOURS=24
PORT=8080
ایک ہی کیز کے ساتھ ایک .env.example بنائیں لیکن کوئی قدر نہیں۔ یہ ہے جو آپ Git سے عہد کرتے ہیں:
DB_HOST=
DB_PORT=
DB_NAME=
DB_USER=
DB_PASSWORD=
JWT_SECRET=
JWT_EXPIRY_HOURS=
PORT=
env کو .gitignore میں شامل کریں۔
.env
lib/config/env.dart بنائیں:
import 'package:dotenv/dotenv.dart';
class Env {
static late final DotEnv _env;
static void load() {
_env = DotEnv(includePlatformEnvironment: true)..load();
}
static String get dbHost => _env['DB_HOST'] ?? 'localhost';
static int get dbPort => int.parse(_env['DB_PORT'] ?? '5432');
static String get dbName => _env['DB_NAME'] ?? 'user_profile_api';
static String get dbUser => _env['DB_USER'] ?? 'dart_user';
static String get dbPassword => _env['DB_PASSWORD'] ?? '';
static String get jwtSecret => _env['JWT_SECRET'] ?? '';
static int get jwtExpiryHours => int.parse(_env['JWT_EXPIRY_HOURS'] ?? '24');
static int get port => int.parse(_env['PORT'] ?? '8080');
}
includePlatformEnvironment: true کا مطلب یہ ہے کہ Env کلاس .env فائل اور اصل سسٹم کے ماحول کے متغیر دونوں سے پڑھتی ہے، اس لیے وہی کوڈ مقامی طور پر .env فائل کا استعمال کرتے ہوئے اور انجکشن شدہ ماحولیاتی متغیرات کا استعمال کرتے ہوئے پروڈکشن میں کام کرے گا۔
لیتھ کلیدی تصورات
API بنانے سے پہلے ہر شیلف کے تصور کو اچھی طرح سمجھنا ضروری ہے۔ اس کا مطلب ہے کہ آپ کو نہ صرف یہ جاننے کی ضرورت ہے کہ یہ کیا کرتا ہے، بلکہ یہ بھی کہ اسے اس طرح کیوں ڈیزائن کیا گیا تھا۔
ہینڈلر
ہینڈلر شیلف کی سب سے بنیادی اکائی ہے۔ یہ صرف ایک فنکشن ہے:
import 'package:shelf/shelf.dart';
Response helloHandler(Request request) {
return Response.ok('Hello, Dart backend!');
}
درخواست ان پٹ، رسپانس آؤٹ پٹ۔ یہ ساری ڈیل ہے۔ ہر اختتامی نقطہ جو آپ بناتے ہیں ایک ہینڈلر ہے۔ تمام مڈل ویئر ایک فنکشن ہے جو ایک ہینڈلر لیتا ہے اور ہینڈلر واپس کرتا ہے۔
ہینڈلرز غیر مطابقت پذیر ہوسکتے ہیں۔
Future getUserHandler(Request request) async {
final users = await userRepository.findAll();
return Response.ok(jsonEncode(users));
}
درخواست اور جواب
درخواست آنے والی HTTP کال کے بارے میں تمام معلومات فراہم کرتی ہے۔
Future handler(Request request) async {
// URL and path
print(request.url); // the full URL
print(request.url.path); // just the path
// Path parameters (when using shelf_router)
final id = request.params['id'];
// Query parameters
final page = request.url.queryParameters['page'];
// Headers
final auth = request.headers['authorization'];
// Body
final body = await request.readAsString();
final json = jsonDecode(body) as Map;
return Response.ok('handled');
}
جواب میں کامن اسٹیٹس کوڈز کے لیے کنسٹرکٹرز کا نام دیا گیا ہے۔
Response.ok(body) // 200
Response.notFound(body) // 404
Response(201, body: body) // any status code
Response(400, body: body) // bad request
Response(401, body: body) // unauthorized
Response(500, body: body) // server error
JSON واپس کرتے وقت ہمیشہ مواد کی قسم کا ہیڈر سیٹ کریں۔
Response.ok(
jsonEncode({'message': 'success'}),
headers: {'Content-Type': 'application/json'},
)
راؤٹر
Shelf_router URL پیٹرنز اور HTTP طریقوں کو ہینڈلرز کے لیے نقشہ بناتا ہے۔
import 'package:shelf_router/shelf_router.dart';
final router = Router();
router.get('/users', getAllUsersHandler);
router.get('/users/', getUserHandler);
router.post('/users', createUserHandler);
router.put('/users/', updateUserHandler);
router.delete('/users/', deleteUserHandler);
نحو راستے کے پیرامیٹرز کی وضاحت کرتا ہے۔ درخواست کے ذریعے ہینڈلر کے اندر تک رسائی حاصل کی گئی۔['id'].
پائپ لائنز اور مڈل ویئر
پائپ لائن مڈل ویئر کو آخر میں ایک ہینڈلر کے ساتھ جوڑتی ہے۔
import 'package:shelf/shelf.dart';
final handler = Pipeline()
.addMiddleware(loggerMiddleware())
.addMiddleware(errorMiddleware())
.addMiddleware(authMiddleware())
.addHandler(router.call);
مڈل ویئر مندرجہ ذیل دستخط کے ساتھ ایک فنکشن ہے:
Middleware myMiddleware() {
return (Handler innerHandler) {
return (Request request) async {
// Before the handler runs
print('Request received: \({request.method} \){request.url}');
final response = await innerHandler(request);
// After the handler runs
print('Response sent: ${response.statusCode}');
return response;
};
};
}
بیرونی افعال مڈل ویئر واپس کرتے ہیں۔ وہ مڈل ویئر ایک فنکشن ہے جو اگلے ہینڈلر کو سلسلہ میں لے جاتا ہے اور ایک نیا ہینڈلر واپس کرتا ہے۔ یہ نیسٹنگ مڈل ویئر کو اندرونی ہینڈلر سے پہلے یا بعد میں کوڈ پر عمل کرنے کی اجازت دیتا ہے۔
PostgreSQL سے جڑیں۔
ڈیٹا بیس کنکشن مینیجر
lib/config/database.dart بنائیں:
import 'package:postgres/postgres.dart';
import 'env.dart';
class Database {
static Connection? _connection;
static Future get connection async {
if (_connection != null) return _connection!;
_connection = await _connect();
return _connection!;
}
static Future _connect() async {
final conn = await Connection.open(
Endpoint(
host: Env.dbHost,
port: Env.dbPort,
database: Env.dbName,
username: Env.dbUser,
password: Env.dbPassword,
),
settings: const ConnectionSettings(
sslMode: SslMode.disable,
),
);
print('✅ Database connected: \({Env.dbHost}:\){Env.dbPort}/${Env.dbName}');
return conn;
}
static Future close() async {
await _connection?.close();
_connection = null;
}
}
یہ سنگلٹن کنکشن مینیجر ہے، وہی پیٹرن جو فلٹر انجینئرز مشترکہ خدمات کے لیے استعمال کرتے ہیں۔ پہلی رسائی پر ایک بار ایک کنکشن بنایا جاتا ہے اور بعد میں آنے والی تمام ڈیٹا بیس کالز کے لیے دوبارہ استعمال کیا جاتا ہے۔
ہجرت چلائیں۔
ایک مائیگریشن فولڈر اور ایس کیو ایل فائل بنائیں۔
Migration/001_create_users.sql:
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
migration/002_create_profiles.sql:
CREATE TABLE IF NOT EXISTS profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
bio TEXT,
avatar_url VARCHAR(500),
phone VARCHAR(20),
location VARCHAR(255),
website VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id)
);
CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id);
lib/config/database.dart میں مائیگریشن ایگزیکیوٹر بنائیں۔
static Future runMigrations() async {
final conn = await connection;
final migrationsDir = Directory('migrations');
final files = migrationsDir
.listSync()
.whereType()
.where((f) => f.path.endsWith('.sql'))
.toList()
..sort((a, b) => a.path.compareTo(b.path));
for (final file in files) {
final sql = await file.readAsString();
await conn.execute(sql);
print('✅ Migration applied: ${file.path}');
}
}
ایک API بنانا
اب جبکہ ڈیٹا بیس منسلک ہے اور منتقلی مکمل ہو گئی ہے، ہم اصل API پرت بنا سکتے ہیں۔
یہ سیکشن صارفین اور پروفائلز دونوں کے لیے ماڈلز، ریپوزٹریز، اور ہینڈلرز کا احاطہ کرتا ہے۔ ماڈل ڈیٹا کی شکل کی وضاحت کرتا ہے، ریپوزٹری ڈیٹا بیس کے تمام تعاملات کو ہینڈل کرتی ہے، اور ہینڈلر HTTP درخواستوں کو ریپوزٹری کالز میں تبدیل کرتا ہے اور کلائنٹ کو جوابات واپس بھیجتا ہے۔ آئیے پہلے صارف کی تہہ بنائیں اور پھر اس کے اوپر پروفائل کی تہہ بنائیں۔
صارف ماڈل
یوزر ماڈل ڈیٹا بیس میں ایک صارف کے ریکارڈ کی نمائندگی کرتا ہے۔ یہ ہجرت میں بنائے گئے صارف کی میزوں پر براہ راست نقشہ بناتا ہے اور ڈیٹا بیس کی قطاروں اور ڈارٹ آبجیکٹ کے درمیان دو طرفہ تبدیلی کو ہینڈل کرتا ہے۔
lib/models/user.dart بنائیں:
class User {
final String id;
final String email;
final String passwordHash;
final String firstName;
final String lastName;
final bool isActive;
final DateTime createdAt;
final DateTime updatedAt;
const User({
required this.id,
required this.email,
required this.passwordHash,
required this.firstName,
required this.lastName,
required this.isActive,
required this.createdAt,
required this.updatedAt,
});
factory User.fromRow(Map row) => User(
id: row['id'] as String,
email: row['email'] as String,
passwordHash: row['password_hash'] as String,
firstName: row['first_name'] as String,
lastName: row['last_name'] as String,
isActive: row['is_active'] as bool,
createdAt: row['created_at'] as DateTime,
updatedAt: row['updated_at'] as DateTime,
);
// Never include passwordHash in JSON responses
Map toJson() => {
'id': id,
'email': email,
'firstName': firstName,
'lastName': lastName,
'isActive': isActive,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
fromRow Maps PostgreSQL نتائج کی قطاریں صارفین کے لیے۔ toJson جان بوجھ کر پاسورڈ ہیش کو خارج کرتا ہے۔ API کے جوابات کو پاس ورڈ ڈیٹا واپس نہیں کرنا چاہیے۔
صارف ذخیرہ
UserRepository آپ کی ایپلیکیشن اور یوزر ٹیبل کے درمیان رابطے کا واحد نقطہ ہے۔ صارف کے لیے ڈیٹا بیس کے تمام آپریشنز یہاں کیے جاتے ہیں، ایس کیو ایل کو شامل کیا جاتا ہے اور ہینڈلرز کو صاف رکھا جاتا ہے۔
lib/repositories/user_repository.dart بنائیں:
import 'dart:async';
import 'package:postgres/postgres.dart';
import '../config/database.dart';
import '../models/user.dart';
class UserRepository {
Future get _conn => Database.connection;
Future> findAll() async {
final conn = await _conn;
final results = await conn.execute(
'SELECT * FROM users WHERE is_active = TRUE ORDER BY created_at DESC',
);
return results.map((row) => User.fromRow(row.toColumnMap())).toList();
}
Future findById(String id) async {
final conn = await _conn;
final results = await conn.execute(
Sql.named('SELECT * FROM users WHERE id = @id AND is_active = TRUE'),
parameters: {'id': id},
);
if (results.isEmpty) return null;
return User.fromRow(results.first.toColumnMap());
}
Future findByEmail(String email) async {
final conn = await _conn;
final results = await conn.execute(
Sql.named('SELECT * FROM users WHERE email = @email'),
parameters: {'email': email},
);
if (results.isEmpty) return null;
return User.fromRow(results.first.toColumnMap());
}
Future create({
required String email,
required String passwordHash,
required String firstName,
required String lastName,
}) async {
final conn = await _conn;
final results = await conn.execute(
Sql.named('''
INSERT INTO users (email, password_hash, first_name, last_name)
VALUES (@email, @passwordHash, @firstName, @lastName)
RETURNING *
'''),
parameters: {
'email': email,
'passwordHash': passwordHash,
'firstName': firstName,
'lastName': lastName,
},
);
return User.fromRow(results.first.toColumnMap());
}
Future update({
required String id,
String? firstName,
String? lastName,
}) async {
final conn = await _conn;
final results = await conn.execute(
Sql.named('''
UPDATE users
SET
first_name = COALESCE(@firstName, first_name),
last_name = COALESCE(@lastName, last_name),
updated_at = NOW()
WHERE id = @id AND is_active = TRUE
RETURNING *
'''),
parameters: {
'id': id,
'firstName': firstName,
'lastName': lastName,
},
);
if (results.isEmpty) return null;
return User.fromRow(results.first.toColumnMap());
}
Future delete(String id) async {
final conn = await _conn;
final results = await conn.execute(
Sql.named('''
UPDATE users SET is_active = FALSE, updated_at = NOW()
WHERE id = @id AND is_active = TRUE
RETURNING id
'''),
parameters: {'id': id},
);
return results.isNotEmpty;
}
}
یہاں چند باتیں قابل توجہ ہیں۔ Sql.named پوزیشنل پیرامیٹرز کے بجائے نامزد پیرامیٹرز (@paramName) استعمال کرتا ہے۔ یہ ایس کیو ایل انجیکشن کو روکتا ہے اور استفسار کو پڑھنے کے قابل بناتا ہے۔
مزید برآں، ڈیلیٹ آپریشن ایک نرم ڈیلیٹ ہے۔ قطار کو ہٹانے کے بجائے، is_active = FALSE سیٹ کریں۔ یہ ایک معیاری پیداوار کا طریقہ ہے۔ آپ کا ڈیٹا درحقیقت ڈیلیٹ نہیں کیا گیا بلکہ غیر فعال کر دیا گیا ہے۔
اپ ڈیٹ پر COALESCE(@firstName, first_name) کا مطلب ہے کہ اگر فراہم کی گئی ہو تو نئی قدر استعمال کریں، بصورت دیگر پرانی قدر رکھیں۔ یہ ہر بار تمام فیلڈز کی ضرورت کے بغیر جزوی اپ ڈیٹس کو صاف طور پر ہینڈل کرتا ہے۔
صارف ہینڈلر
یوزر ہینڈلر کلاس ریپوزٹری آپریشنز کو HTTP اینڈ پوائنٹ کے طور پر بے نقاب کرتا ہے۔ اندرونی طور پر، یہ ایک راؤٹر مثال کا مالک ہے اور روٹنگ منطق اور ہینڈلر لاجک کو ایک جگہ پر رکھتے ہوئے ہر راستے کو نجی طریقہ پر نقشہ بناتا ہے۔
lib/handlers/user_handler.dart بنائیں:
import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../repositories/user_repository.dart';
class UserHandler {
final UserRepository _repository;
UserHandler(this._repository);
Router get router {
final router = Router();
router.get('/', _getAll);
router.get('/', _getOne);
router.put('/', _update);
router.delete('/', _delete);
return router;
}
Future _getAll(Request request) async {
final users = await _repository.findAll();
return Response.ok(
jsonEncode(users.map((u) => u.toJson()).toList()),
headers: {'Content-Type': 'application/json'},
);
}
Future _getOne(Request request, String id) async {
final user = await _repository.findById(id);
if (user == null) {
return Response.notFound(
jsonEncode({'error': 'User not found'}),
headers: {'Content-Type': 'application/json'},
);
}
return Response.ok(
jsonEncode(user.toJson()),
headers: {'Content-Type': 'application/json'},
);
}
Future _update(Request request, String id) async {
final body = jsonDecode(await request.readAsString()) as Map;
final user = await _repository.update(
id: id,
firstName: body['firstName'] as String?,
lastName: body['lastName'] as String?,
);
if (user == null) {
return Response.notFound(
jsonEncode({'error': 'User not found'}),
headers: {'Content-Type': 'application/json'},
);
}
return Response.ok(
jsonEncode(user.toJson()),
headers: {'Content-Type': 'application/json'},
);
}
Future _delete(Request request, String id) async {
final deleted = await _repository.delete(id);
if (!deleted) {
return Response.notFound(
jsonEncode({'error': 'User not found'}),
headers: {'Content-Type': 'application/json'},
);
}
return Response(
204,
headers: {'Content-Type': 'application/json'},
);
}
}
پروفائل ماڈل
پروفائل ماڈل کسی صارف کے بارے میں توسیع شدہ معلومات کی نمائندگی کرتا ہے جو صارف کی بنیادی تاریخ سے الگ محفوظ کی جاتی ہے۔ ون ٹو ون رشتہ پروفائل ٹیبل میں user_id پر ایک منفرد انڈیکس کے ذریعے نافذ کیا جاتا ہے۔ چونکہ پروفائلز جزوی معلومات کے ساتھ بنائے جاتے ہیں اور وقت کے ساتھ ساتھ آباد ہو سکتے ہیں، اس لیے userId کے علاوہ تمام فیلڈز null کی اجازت دیتے ہیں۔
lib/models/profile.dart بنائیں:
class Profile {
final String id;
final String userId;
final String? bio;
final String? avatarUrl;
final String? phone;
final String? location;
final String? website;
final DateTime createdAt;
final DateTime updatedAt;
const Profile({
required this.id,
required this.userId,
this.bio,
this.avatarUrl,
this.phone,
this.location,
this.website,
required this.createdAt,
required this.updatedAt,
});
factory Profile.fromRow(Map row) => Profile(
id: row['id'] as String,
userId: row['user_id'] as String,
bio: row['bio'] as String?,
avatarUrl: row['avatar_url'] as String?,
phone: row['phone'] as String?,
location: row['location'] as String?,
website: row['website'] as String?,
createdAt: row['created_at'] as DateTime,
updatedAt: row['updated_at'] as DateTime,
);
Map toJson() => {
'id': id,
'userId': userId,
'bio': bio,
'avatarUrl': avatarUrl,
'phone': phone,
'location': location,
'website': website,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
پروفائل اسٹور
ProfileRepository پروفائل ٹیبلز کے لیے تمام ڈیٹا بیس آپریشنز کو ہینڈل کرتی ہے۔ صارف کے ذخیروں کے برعکس جو ID کے ذریعے تلاش کرتے ہیں، زیادہ تر پروفائل آپریشن userId کو تلاش کی کلید کے طور پر استعمال کرتے ہیں۔ اس کی وجہ یہ ہے کہ کلائنٹ اپنی اندرونی ID کے بجائے اس پروفائل کا حوالہ دیتا ہے جس سے اس کا تعلق ہے۔
lib/repositories/profile_repository.dart بنائیں:
import 'package:postgres/postgres.dart';
import '../config/database.dart';
import '../models/profile.dart';
class ProfileRepository {
Future get _conn => Database.connection;
Future findByUserId(String userId) async {
final conn = await _conn;
final results = await conn.execute(
Sql.named('SELECT * FROM profiles WHERE user_id = @userId'),
parameters: {'userId': userId},
);
if (results.isEmpty) return null;
return Profile.fromRow(results.first.toColumnMap());
}
Future create({
required String userId,
String? bio,
String? avatarUrl,
String? phone,
String? location,
String? website,
}) async {
final conn = await _conn;
final results = await conn.execute(
Sql.named('''
INSERT INTO profiles (user_id, bio, avatar_url, phone, location, website)
VALUES (@userId, @bio, @avatarUrl, @phone, @location, @website)
RETURNING *
'''),
parameters: {
'userId': userId,
'bio': bio,
'avatarUrl': avatarUrl,
'phone': phone,
'location': location,
'website': website,
},
);
return Profile.fromRow(results.first.toColumnMap());
}
Future update({
required String userId,
String? bio,
String? avatarUrl,
String? phone,
String? location,
String? website,
}) async {
final conn = await _conn;
final results = await conn.execute(
Sql.named('''
UPDATE profiles
SET
bio = COALESCE(@bio, bio),
avatar_url = COALESCE(@avatarUrl, avatar_url),
phone = COALESCE(@phone, phone),
location = COALESCE(@location, location),
website = COALESCE(@website, website),
updated_at = NOW()
WHERE user_id = @userId
RETURNING *
'''),
parameters: {
'userId': userId,
'bio': bio,
'avatarUrl': avatarUrl,
'phone': phone,
'location': location,
'website': website,
},
);
if (results.isEmpty) return null;
return Profile.fromRow(results.first.toColumnMap());
}
}
پروفائل ہینڈلر
ProfileHandler صارف ID کے تحت اندرون خانہ پروفائل اینڈ پوائنٹس کا انتظام کرتا ہے۔ ہر آپریشن سے پہلے، چیک کریں کہ آیا پیرنٹ صارف موجود ہے۔ آپ غیر موجود صارفین کے لیے پروفائلز نہیں بنا سکتے، درآمد یا اپ ڈیٹ نہیں کر سکتے۔ یہ تخلیق کی اجازت دینے سے پہلے موجودہ ریکارڈز کی جانچ کرکے پروفائل کی نقل کو بھی روکتا ہے۔
lib/handlers/profile_handler.dart بنائیں:
import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../repositories/profile_repository.dart';
import '../repositories/user_repository.dart';
class ProfileHandler {
final ProfileRepository _profileRepository;
final UserRepository _userRepository;
ProfileHandler(this._profileRepository, this._userRepository);
Router get router {
final router = Router();
router.get('//profile', _getProfile);
router.post('//profile', _createProfile);
router.put('//profile', _updateProfile);
return router;
}
Future _getProfile(Request request, String userId) async {
final user = await _userRepository.findById(userId);
if (user == null) {
return Response.notFound(
jsonEncode({'error': 'User not found'}),
headers: {'Content-Type': 'application/json'},
);
}
final profile = await _profileRepository.findByUserId(userId);
if (profile == null) {
return Response.notFound(
jsonEncode({'error': 'Profile not found'}),
headers: {'Content-Type': 'application/json'},
);
}
return Response.ok(
jsonEncode(profile.toJson()),
headers: {'Content-Type': 'application/json'},
);
}
Future _createProfile(Request request, String userId) async {
final user = await _userRepository.findById(userId);
if (user == null) {
return Response.notFound(
jsonEncode({'error': 'User not found'}),
headers: {'Content-Type': 'application/json'},
);
}
final existing = await _profileRepository.findByUserId(userId);
if (existing != null) {
return Response(
409,
body: jsonEncode({'error': 'Profile already exists for this user'}),
headers: {'Content-Type': 'application/json'},
);
}
final body = jsonDecode(await request.readAsString()) as Map;
final profile = await _profileRepository.create(
userId: userId,
bio: body['bio'] as String?,
avatarUrl: body['avatarUrl'] as String?,
phone: body['phone'] as String?,
location: body['location'] as String?,
website: body['website'] as String?,
);
return Response(
201,
body: jsonEncode(profile.toJson()),
headers: {'Content-Type': 'application/json'},
);
}
Future _updateProfile(Request request, String userId) async {
final body = jsonDecode(await request.readAsString()) as Map;
final profile = await _profileRepository.update(
userId: userId,
bio: body['bio'] as String?,
avatarUrl: body['avatarUrl'] as String?,
phone: body['phone'] as String?,
location: body['location'] as String?,
website: body['website'] as String?,
);
if (profile == null) {
return Response.notFound(
jsonEncode({'error': 'Profile not found'}),
headers: {'Content-Type': 'application/json'},
);
}
return Response.ok(
jsonEncode(profile.toJson()),
headers: {'Content-Type': 'application/json'},
);
}
}
ثبوت
ایک بار جب بنیادی صارف اور پروفائل CRUD تیار ہو جائے تو اگلا مرحلہ API کو محفوظ کرنا ہے۔
اس پروجیکٹ میں سرٹیفیکیشن دو حصوں میں کام کرتا ہے۔ AuthService خفیہ نگاری کے کاموں کو سنبھالتی ہے، بشمول پاس ورڈ ہیشنگ، JWT تخلیق اور تصدیق، اور AuthHandler رجسٹریشن اور لاگ ان اینڈ پوائنٹس کو بے نقاب کرتا ہے جنہیں کلائنٹ ٹوکن حاصل کرنے کے لیے کال کرتے ہیں۔ ٹوکن جاری ہونے کے بعد، AuthMiddleware ہینڈلر تک پہنچنے سے پہلے ہر محفوظ درخواست کے لیے ٹوکن کی توثیق کرتا ہے۔
پاس ورڈ ہیشنگ
lib/services/auth_service.dart بنائیں:
import 'package:bcrypt/bcrypt.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import '../config/env.dart';
import '../models/user.dart';
class AuthService {
String hashPassword(String password) {
return BCrypt.hashpw(password, BCrypt.gensalt());
}
bool verifyPassword(String password, String hash) {
return BCrypt.checkpw(password, hash);
}
String generateToken(User user) {
final jwt = JWT(
{
'sub': user.id,
'email': user.email,
'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
},
);
return jwt.sign(
SecretKey(Env.jwtSecret),
expiresIn: Duration(hours: Env.jwtExpiryHours),
);
}
JWT? verifyToken(String token) {
try {
return JWT.verify(token, SecretKey(Env.jwtSecret));
} catch (_) {
return null;
}
}
}
BCrypt.hashpw نمکین ہیش تیار کرتا ہے۔ BCrypt.checkpw ذخیرہ شدہ ہیش کے خلاف سادہ پاس ورڈ چیک کرتا ہے۔ نمک ہیش میں بنایا جاتا ہے اور اسے الگ سے ذخیرہ نہیں کیا جاتا ہے۔
verifyToken ناکامی، میعاد ختم ہونے والے ٹوکن، غلط دستخط، یا خراب ٹوکن پر پھینکنے کے بجائے null لوٹاتا ہے۔ یہ توثیق مڈل ویئر کو صاف رکھتا ہے۔
تصدیق ہینڈلر
lib/handlers/auth_handler.dart بنائیں:
import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../repositories/user_repository.dart';
import '../services/auth_service.dart';
class AuthHandler {
final UserRepository _userRepository;
final AuthService _authService;
AuthHandler(this._userRepository, this._authService);
Router get router {
final router = Router();
router.post('/register', _register);
router.post('/login', _login);
return router;
}
Future _register(Request request) async {
final body = jsonDecode(await request.readAsString()) as Map;
final email = body['email'] as String?;
final password = body['password'] as String?;
final firstName = body['firstName'] as String?;
final lastName = body['lastName'] as String?;
if (email == null || password == null || firstName == null || lastName == null) {
return Response(
400,
body: jsonEncode({'error': 'email, password, firstName, and lastName are required'}),
headers: {'Content-Type': 'application/json'},
);
}
if (password.length < 8) {
return Response(
400,
body: jsonEncode({'error': 'Password must be at least 8 characters'}),
headers: {'Content-Type': 'application/json'},
);
}
final existing = await _userRepository.findByEmail(email);
if (existing != null) {
return Response(
409,
body: jsonEncode({'error': 'An account with this email already exists'}),
headers: {'Content-Type': 'application/json'},
);
}
final passwordHash = _authService.hashPassword(password);
final user = await _userRepository.create(
email: email,
passwordHash: passwordHash,
firstName: firstName,
lastName: lastName,
);
final token = _authService.generateToken(user);
return Response(
201,
body: jsonEncode({
'user': user.toJson(),
'token': token,
}),
headers: {'Content-Type': 'application/json'},
);
}
Future _login(Request request) async {
final body = jsonDecode(await request.readAsString()) as Map;
final email = body['email'] as String?;
final password = body['password'] as String?;
if (email == null || password == null) {
return Response(
400,
body: jsonEncode({'error': 'email and password are required'}),
headers: {'Content-Type': 'application/json'},
);
}
final user = await _userRepository.findByEmail(email);
// Deliberately vague error, never confirm whether an email exists
if (user == null || !_authService.verifyPassword(password, user.passwordHash)) {
return Response(
401,
body: jsonEncode({'error': 'Invalid email or password'}),
headers: {'Content-Type': 'application/json'},
);
}
final token = _authService.generateToken(user);
return Response.ok(
jsonEncode({
'user': user.toJson(),
'token': token,
}),
headers: {'Content-Type': 'application/json'},
);
}
}
لاگ ان کی خرابی کے پیغامات جان بوجھ کر مبہم ہیں۔ یہ "ای میل نہیں ملا" یا "غلط پاس ورڈ" نہیں ہے، یہ "غلط ای میل یا پاس ورڈ" ہے۔ اس بات کا تعین کرنے سے کہ کون سا حصہ غلط ہے حملہ آور کو درست اکاؤنٹس کی گنتی میں مدد ملے گی۔
توثیق مڈل ویئر
lib/middleware/auth_middleware.dart بنائیں:
import 'dart:convert';
import 'package:shelf/shelf.dart';
import '../services/auth_service.dart';
Middleware authMiddleware(AuthService authService) {
return (Handler innerHandler) {
return (Request request) async {
final authHeader = request.headers['authorization'];
if (authHeader == null || !authHeader.startsWith('Bearer ')) {
return Response(
401,
body: jsonEncode({'error': 'Authorization header missing or malformed'}),
headers: {'Content-Type': 'application/json'},
);
}
final token = authHeader.substring(7); // Remove 'Bearer '
final jwt = authService.verifyToken(token);
if (jwt == null) {
return Response(
401,
body: jsonEncode({'error': 'Invalid or expired token'}),
headers: {'Content-Type': 'application/json'},
);
}
// Attach the user ID to the request context for downstream handlers
final updatedRequest = request.change(
context: {
...request.context,
'userId': jwt.payload['sub'] as String,
'userEmail': jwt.payload['email'] as String,
},
);
return innerHandler(updatedRequest);
};
};
}
request.change(context: {...}) یہ ہے کہ کس طرح شیلف مڈل ویئر سے ہینڈلر کو ڈیٹا منتقل کرتا ہے۔ یہ ایکسپریس یا ASP.NET مڈل ویئر کی درخواست پر ڈیٹا منسلک کرنے کے مترادف ہے۔ نیچے کا کوئی بھی ہینڈلر request.context پڑھ سکتا ہے۔['userId'] چیک کریں کہ کون سے صارفین مستند ہیں۔
ہینڈلنگ میں خرابی
اس سے کوئی فرق نہیں پڑتا ہے کہ آپ اپنے ہینڈلرز کو کتنی ہی احتیاط سے لکھتے ہیں، پیداوار میں غیر متوقع ناکامیاں ہو سکتی ہیں، بشمول غلط درخواست کی باڈیز، ڈیٹا بیس کا ٹائم آؤٹ، اور غیر ہینڈل شدہ استثنائی معاملات۔
ہر ہینڈلر کو انفرادی طور پر اپنے غلطی کے جواب کا انتظام کرنے کے بجائے، ہم ایک ہی مڈل ویئر میں غلطی سے نمٹنے کو مرکزی بنائیں گے جو پوری پائپ لائن کو لپیٹ دیتا ہے۔ یہ تمام اختتامی نقطوں پر مستقل غلطی کے جواب کی شکل کو یقینی بناتا ہے اور اندرونی خرابی کی تفصیلات کو کلائنٹ تک پہنچنے سے روکتا ہے۔
lib/middleware/error_middleware.dart بنائیں:
import 'dart:convert';
import 'package:shelf/shelf.dart';
Middleware errorMiddleware() {
return (Handler innerHandler) {
return (Request request) async {
try {
return await innerHandler(request);
} on FormatException catch (e) {
return Response(
400,
body: jsonEncode({'error': 'Invalid request body: ${e.message}'}),
headers: {'Content-Type': 'application/json'},
);
} catch (e, stackTrace) {
// Log the full error and stack trace server-side
print('Unhandled error: $e');
print(stackTrace);
// Never expose internal error details to the client
return Response(
500,
body: jsonEncode({'error': 'An internal server error occurred'}),
headers: {'Content-Type': 'application/json'},
);
}
};
};
}
lib/middleware/logger_middleware.dart بنائیں:
import 'package:shelf/shelf.dart';
Middleware loggerMiddleware() {
return (Handler innerHandler) {
return (Request request) async {
final start = DateTime.now();
final response = await innerHandler(request);
final duration = DateTime.now().difference(start).inMilliseconds;
print(
'[${DateTime.now().toIso8601String()}] '
'\({request.method} \){request.url.path} '
'→ \({response.statusCode} (\){duration}ms)',
);
return response;
};
};
}
ہر چیز کو آپس میں جوڑنا
ہینڈلر، ریپوزٹری، اور مڈل ویئر کے ساتھ، آخری مرحلہ ان کو چلانے والے ایک سرور سے جوڑنا ہے۔ روٹر ہینڈلرز کے لیے یو آر ایل کے سابقے نقشے کرتا ہے، پائپ لائن مڈل ویئر کو درست ترتیب میں اسٹیک کرتی ہے، اور انٹری پوائنٹ ہر چیز کو ترتیب سے بوٹ کرتا ہے: ماحول کے متغیرات کو لوڈ کرنا، منتقلی چلانا، اور سرور شروع کرنا۔
lib/router.dart بنائیں:
import 'package:shelf_router/shelf_router.dart';
import 'handlers/auth_handler.dart';
import 'handlers/user_handler.dart';
import 'handlers/profile_handler.dart';
import 'middleware/auth_middleware.dart';
import 'repositories/user_repository.dart';
import 'repositories/profile_repository.dart';
import 'services/auth_service.dart';
Router createRouter() {
final userRepository = UserRepository();
final profileRepository = ProfileRepository();
final authService = AuthService();
final authHandler = AuthHandler(userRepository, authService);
final userHandler = UserHandler(userRepository);
final profileHandler = ProfileHandler(profileRepository, userRepository);
final router = Router();
// Public routes, no auth required
router.mount('/auth', authHandler.router.call);
// Protected routes, auth middleware applied
router.mount(
'/users',
Pipeline()
.addMiddleware(authMiddleware(authService))
.addHandler(userHandler.router.call),
);
router.mount(
'/users',
Pipeline()
.addMiddleware(authMiddleware(authService))
.addHandler(profileHandler.router.call),
);
return router;
}
انٹری پوائنٹ bin/server.dart بنائیں:
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import '../lib/config/database.dart';
import '../lib/config/env.dart';
import '../lib/middleware/error_middleware.dart';
import '../lib/middleware/logger_middleware.dart';
import '../lib/router.dart';
void main() async {
// Load environment variables
Env.load();
// Run database migrations
await Database.runMigrations();
// Build the handler pipeline
final router = createRouter();
final handler = Pipeline()
.addMiddleware(errorMiddleware())
.addMiddleware(loggerMiddleware())
.addHandler(router.call);
// Start the server
final server = await shelf_io.serve(
handler,
InternetAddress.anyIPv4,
Env.port,
);
print('🚀 Server running on port ${server.port}');
}
سرور چلائیں:
dart run bin/server.dart
# ✅ Database connected: localhost:5432/user_profile_api
# ✅ Migration applied: migrations/001_create_users.sql
# ✅ Migration applied: migrations/002_create_profiles.sql
# 🚀 Server running on port 8080
تعیناتی
سرور مقامی طور پر چل رہا ہے اور تمام اختتامی نقطے اوپر ہیں۔ اب جہاز بھیجنے کا وقت آگیا ہے۔
ہم تعیناتی کے دو راستوں کا احاطہ کریں گے: پہلے، مقامی پروڈکشن ٹیسٹنگ کے لیے ڈوکر کمپوز کے ساتھ ایپ اور ڈیٹا بیس کو پیک کریں، پھر اسے Fly.io پر تعینات کریں، جہاں ایک منظم PostgreSQL ڈیٹا بیس اور خودکار TLS کا استعمال کرتے ہوئے API انٹرنیٹ پر قابل رسائی ہے۔
ڈاکر فائل
پروجیکٹ روٹ میں ایک ڈاکر فائل بنائیں۔
FROM dart:stable AS build
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get
COPY . .
RUN dart compile exe bin/server.dart -o bin/server
FROM debian:stable-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /app/bin/server bin/server
COPY --from=build /app/migrations migrations/
EXPOSE 8080
CMD ["bin/server"]
یہ ایک کثیر مرحلہ تعمیر ہے۔ پہلا مرحلہ سرور کو مقامی بائنریز میں مرتب کرنے کے لیے مکمل ڈارٹ SDK امیج کا استعمال کرتا ہے۔ دوسرے مرحلے میں، ہم صرف مرتب شدہ بائنریز کاپی کرتے ہیں اور انہیں کم سے کم ڈیبین امیج میں منتقل کرتے ہیں۔ کوئی ڈارٹ SDK، سورس کوڈ، یا بلڈ ٹولز نہیں ہے۔ حتمی تصویر جامع اور پروڈکشن کے لیے تیار ہے۔
مقامی پیداوار کی جانچ کے لیے ڈوکر کمپوز
ڈیٹا بیس کے ساتھ ایپ کو شامل کرنے کے لیے docker-compose.yml کو اپ ڈیٹ کریں۔
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: user_profile_db
environment:
POSTGRES_DB: user_profile_api
POSTGRES_USER: dart_user
POSTGRES_PASSWORD: dart_password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dart_user -d user_profile_api"]
interval: 5s
timeout: 5s
retries: 5
api:
build: .
container_name: user_profile_api
ports:
- "8080:8080"
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: user_profile_api
DB_USER: dart_user
DB_PASSWORD: dart_password
JWT_SECRET: local_test_secret_replace_in_production
JWT_EXPIRY_HOURS: 24
PORT: 8080
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
پوسٹگریس سروسز میں صحت کی جانچ اس بات کو یقینی بناتی ہے کہ API کنٹینر صرف اس وقت شروع ہوتا ہے جب ڈیٹا بیس کنکشن کو قبول کرنے کے لیے تیار ہو (جب خدمات بیک وقت شروع ہوتی ہیں تو پیداوار کا ایک عام مسئلہ)۔
سب کچھ بنائیں اور چلائیں۔
docker compose up --build
Fly.io پر تعینات کریں۔
Fly.io کنٹینرائزڈ بیک اینڈ سروسز کے لیے صاف ترین تعیناتی اہداف میں سے ایک ہے۔ عالمی تقسیم، خودکار TLS، اور نظم شدہ PostgreSQL ڈیٹا بیس کو ہینڈل کرتا ہے۔
مرحلہ 1 - تنصیب اور سرٹیفیکیشن:
# macOS
brew install flyctl
# Authenticate
fly auth login
مرحلہ 2 - ایپ چلائیں:
fly launch
Fly خود بخود آپ کی Dockerfile کا پتہ لگاتا ہے اور آپ کے ایپ کا نام، علاقہ اور کیا آپ PostgreSQL ڈیٹا بیس بنانا چاہتے ہیں سمیت چند سوالات پوچھتا ہے۔ اگر آپ PostgreSQL پرامپٹ کا جواب ہاں میں دیتے ہیں تو Fly آپ کے زیر انتظام ڈیٹا بیس کو فراہم کرے گا اور خودکار طور پر کنکشن سٹرنگ داخل کر دے گا۔
مرحلہ 3 - ماحولیاتی متغیرات سیٹ کریں:
fly secrets set JWT_SECRET="your_production_secret_here"
fly secrets set JWT_EXPIRY_HOURS="24"
جب آپ PostgreSQL کلسٹر فراہم کرتے ہیں تو ڈیٹا بیس کنکشن متغیرات خود بخود Fly کے ذریعے سیٹ ہو جاتے ہیں۔
مرحلہ 4 - تعینات کریں:
fly deploy
فلائی ڈوکر امیجز بناتا ہے، انہیں آپ کی رجسٹری میں دھکیلتا ہے، اور انہیں اپنی پسند کے علاقے میں تعینات کرتا ہے۔ مکمل ہونے کے بعد:
fly status
# Your app is running at https://your-app-name.fly.dev
مرحلہ 5 - تعیناتی کی تصدیق کریں:
curl https://your-app-name.fly.dev/auth/register \
-X POST \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123","firstName":"Seyi","lastName":"Dev"}'
API ٹیسٹنگ
اگر آپ کا سرور مقامی طور پر پورٹ 8080 پر چل رہا ہے، تو اس بات کو یقینی بنانے کے لیے کہ ہر چیز اختتام سے آخر تک کام کرتی ہے، مکمل بہاؤ درج ذیل ہے:
صارف کی رجسٹریشن:
curl http://localhost:8080/auth/register \
-X POST \
-H "Content-Type: application/json" \
-d '{
"email": "seyi@example.com",
"password": "securepassword",
"firstName": "Seyi",
"lastName": "Dev"
}'
جواب:
{
"user": {
"id": "uuid-here",
"email": "seyi@example.com",
"firstName": "Seyi",
"lastName": "Dev",
"isActive": true,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"token": "eyJhbGci..."
}
لاگ ان:
curl http://localhost:8080/auth/login \
-X POST \
-H "Content-Type: application/json" \
-d '{"email": "seyi@example.com", "password": "securepassword"}'
تمام صارفین حاصل کریں (تصدیق شدہ):
curl http://localhost:8080/users \
-H "Authorization: Bearer eyJhbGci..."
اپنا پروفائل بنائیں:
curl http://localhost:8080/users/{userId}/profile \
-X POST \
-H "Authorization: Bearer eyJhbGci..." \
-H "Content-Type: application/json" \
-d '{
"bio": "Flutter engineer turned backend developer",
"location": "Lagos, Nigeria",
"website": "https://example.com"
}'
صارف کی تازہ کارییں:
curl http://localhost:8080/users/{userId} \
-X PUT \
-H "Authorization: Bearer eyJhbGci..." \
-H "Content-Type: application/json" \
-d '{"firstName": "Oluwaseyi"}'
صارف کو حذف کریں:
curl http://localhost:8080/users/{userId} \
-X DELETE \
-H "Authorization: Bearer eyJhbGci..."
نتیجہ
ہم نے Dart میں ایک پروڈکشن گریڈ REST API بنایا اور تعینات کیا، وہی زبان جو ہم پہلے سے Flutter سے جانتے ہیں۔ کوئی نئی زبان نہیں، کوئی نیا نمونہ نہیں۔ ڈارٹ مختلف سیاق و سباق میں چلتا ہے۔
شیلف ذہنی ماڈل (ہینڈلر، مڈل ویئر، پائپ لائنز، روٹرز) جان بوجھ کر کم سے کم ہے۔ یہ آپ کے لیے فیصلے نہیں کرتا ہے۔ یہ قابل ترتیب پرائمیٹوز فراہم کرتا ہے اور آپ کو ان کو بالکل اسی فن تعمیر میں جوڑنے دیتا ہے جس کی آپ کے پروجیکٹ کی ضرورت ہے۔ یہ فلسفہ فلٹر انجینئرز کو واقف محسوس کرے گا، جو نسخے کے فریم ورک پر بھروسہ کیے بغیر اپنا صاف ستھرا فن تعمیر بناتے ہیں۔
یہاں بنائے گئے ماڈلز، ریپوزٹریز، سروسز، ہینڈلرز، اور مڈل ویئر بیک اینڈ پر خدشات کی وہی علیحدگی لاگو کرتے ہیں جیسا کہ فلٹر میں لاگو ہوتا ہے۔ تصور منتقل کیا جاتا ہے۔ ڈارٹ ٹیکنالوجی کی منتقلی فن تعمیر کا میدان تبدیل ہو رہا ہے۔
اس کے ذریعے، آپ سمجھیں گے کہ ڈارٹ ایک طاقتور زبان ہے جو فرنٹ اینڈ اور بیک اینڈ ایکو سسٹم دونوں پر محیط ہے۔ شیلف کے علاوہ، ڈارٹ فراگ اور سرور پوڈ ہیں جو اب بھی بیک اینڈ سائیڈ پر اچھی طرح کام کرتے ہیں۔ مستقبل کے مضمون میں اس پر مزید۔
ہاں، اسے آزمائیں اور بعد میں میرا شکریہ!