نقاط النهاية والنماذج
المسارات والبيانات
نقاط النهاية والنماذج درس مجاني في Learn Rust Coding على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Learn Rust Coding، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Learn Rust Coding 4 دروس في المجموع.
المسارات ونماذج البيانات
تُعرَّف واجهة API من خلال نقاط النهاية الخاصة بها (عناوين URL مع طرق HTTP)، ومن خلال النماذج (أشكال البيانات) التي تمر عبرها. في هذا الدرس، ستعرّف نماذج الطلبات والاستجابات باستخدام serde، وتربط مسارات بأسلوب CRUD في Axum.
تعريف نموذج
النموذج هو بنية Rust عادية. اشتق Serialize حتى يمكن تحويله إلى JSON في الاستجابات، وDeserialize حتى يمكن تحليله من أجسام الطلبات. وتُطابق أسماء الحقول مفاتيح JSON مباشرةً.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone)]
struct Todo {
id: u32,
title: String,
done: bool,
}فصل أنواع الطلبات عن أنواع الاستجابات
ينبغي ألا يرسل العملاء id الذي يعيّنه الخادم عند إنشاء مورد. استخدم بنية input منفصلة لجسم الطلب، واستخدم النموذج الكامل للاستجابات. يحافظ ذلك على وضوح العقد ويمنع العملاء من تعيين حقول لا ينبغي لهم تعيينها.
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateTodo {
title: String,
}
// The handler assigns the id and sets done = false.GET: عرض الموارد
يعيد معالج GET /todos المجموعة كاملةً على هيئة مصفوفة JSON. ويقرأ الحالة المشتركة، وينسخ البيانات خارج القفل بسرعة، ثم يغلّفها داخل Json.
use axum::{extract::State, Json};
use std::sync::{Arc, Mutex};
type Store = Arc<Mutex<Vec<Todo>>>;
#[derive(Clone, serde::Serialize)]
struct Todo { id: u32, title: String, done: bool }
async fn list_todos(State(store): State<Store>) -> Json<Vec<Todo>> {
let todos = store.lock().unwrap().clone();
Json(todos)
}POST: إنشاء مورد
يقرأ معالج POST /todos جسم JSON باستخدام مستخرج Json، ويعيّن معرّفًا جديدًا، ويخزّن العنصر، ثم يعيده مع الحالة 201 Created. يتيح لك الصف (StatusCode, Json<T>) تعيين كلٍّ من الحالة والجسم.
use axum::{extract::State, Json, http::StatusCode};
async fn create_todo(
State(store): State<Store>,
Json(input): Json<CreateTodo>,
) -> (StatusCode, Json<Todo>) {
let mut todos = store.lock().unwrap();
let id = todos.len() as u32 + 1;
let todo = Todo { id, title: input.title, done: false };
todos.push(todo.clone());
(StatusCode::CREATED, Json(todo))
}معاملات المسار
لجلب مورد واحد، التقط جزءًا من عنوان URL باستخدام معامل مسار. صرّح عنه في المسار بالشكل /todos/{id} واستخرجه باستخدام مستخرج Path. ويُحلَّل النوع الذي تطلبه، وهو u32 هنا، تلقائيًا.
use axum::extract::{Path, State};
use axum::{Json, http::StatusCode};
async fn get_todo(
State(store): State<Store>,
Path(id): Path<u32>,
) -> Result<Json<Todo>, StatusCode> {
let todos = store.lock().unwrap();
match todos.iter().find(|t| t.id == id) {
Some(t) => Ok(Json(t.clone())),
None => Err(StatusCode::NOT_FOUND),
}
}معاملات الاستعلام
يستخدم التصفية وتقسيم الصفحات سلسلة الاستعلام، مثل /todos?done=true. التقطها باستخدام مستخرج Query داخل بنية Deserialize. وتستخدم الحقول الاختيارية Option، لذلك لا بأس بغياب المعاملات.
use axum::extract::{Query, State};
use axum::Json;
use serde::Deserialize;
#[derive(Deserialize)]
struct Filter { done: Option<bool> }
async fn filtered(
State(store): State<Store>,
Query(f): Query<Filter>,
) -> Json<Vec<Todo>> {
let todos = store.lock().unwrap();
let out = todos.iter()
.filter(|t| f.done.map_or(true, |d| t.done == d))
.cloned().collect();
Json(out)
}PUT وDELETE
يُجرى التحديث باستخدام PUT /todos/{id} مع جسم، بينما يُجرى الحذف باستخدام DELETE /todos/{id}. يبحث كلاهما عن العنصر حسب المعرّف، ويعيد 404 إذا لم يكن موجودًا. ويعيد الحذف عادةً 204 No Content عند النجاح.
use axum::extract::{Path, State};
use axum::http::StatusCode;
async fn delete_todo(
State(store): State<Store>,
Path(id): Path<u32>,
) -> StatusCode {
let mut todos = store.lock().unwrap();
let before = todos.len();
todos.retain(|t| t.id != id);
if todos.len() < before { StatusCode::NO_CONTENT }
else { StatusCode::NOT_FOUND }
}ربط المسارات معًا
سجّل كل معالج في الموجّه. واجمع الأساليب ضمن المسارات المشتركة: يتولى /todos عمليتي العرض والإنشاء، بينما يتولى /todos/{id} الجلب والتحديث والحذف. أرفق مخزن البيانات المشترك باستخدام .with_state.
use axum::{routing::get, Router};
fn build(store: Store) -> Router {
Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route("/todos/{id}",
get(get_todo).delete(delete_todo))
.with_state(store)
}التحقق من الإدخال
لا تثق ببيانات العميل أبدًا. تحقّق منها داخل المعالج، وأعد 400 Bad Request عندما تكون غير صالحة. نرفض هنا العنوان الفارغ قبل تخزين أي شيء، مما يمنع دخول البيانات التالفة إلى النظام.
use axum::{Json, extract::State, http::StatusCode};
async fn create_validated(
State(store): State<Store>,
Json(input): Json<CreateTodo>,
) -> Result<(StatusCode, Json<Todo>), StatusCode> {
if input.title.trim().is_empty() {
return Err(StatusCode::BAD_REQUEST);
}
let mut todos = store.lock().unwrap();
let id = todos.len() as u32 + 1;
let todo = Todo { id, title: input.title, done: false };
todos.push(todo.clone());
Ok((StatusCode::CREATED, Json(todo)))
}استجابات الأخطاء المتسقة
إعادة رموز الحالة وحدها تنجح، لكن واجهات API الإنتاجية تعيد أيضًا جسم خطأ بصيغة JSON. ومن الأساليب الشائعة استخدام تعداد أخطاء مخصص يطبّق IntoResponse، مع ربط كل متغير بحالة ورسالة. يزوّد ذلك العملاء بأخطاء متوقعة وقابلة للقراءة آليًا.
use axum::response::{IntoResponse, Response};
use axum::http::StatusCode;
use axum::Json;
use serde_json::json;
enum ApiError { NotFound, BadRequest(String) }
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, msg) = match self {
ApiError::NotFound => (StatusCode::NOT_FOUND, "not found".to_string()),
ApiError::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
};
(status, Json(json!({ "error": msg }))).into_response()
}
}اختبار سريع
اختبر فهمك لنقاط النهاية والنماذج.
مراجعة
لقد عرّفت نقاط النهاية والنماذج:
- النماذج هي بنيات تطبّق اشتقاق
Serialize/Deserialize؛ استخدم أنواع إدخال منفصلة. - تستخرج
JsonوPathوQueryوStateبيانات الطلب. - اربط عمليات CRUD بالطلبات GET/POST/PUT/DELETE مع رموز الحالة المناسبة.
- تحقّق من الإدخال وأعد
400عند وجود بيانات غير صالحة. - يوفّر نوع الخطأ المخصص الذي يطبّق
IntoResponseأخطاء JSON متسقة.
الأسئلة الشائعة
هل درس «نقاط النهاية والنماذج» مجاني؟
نعم — نص درس «نقاط النهاية والنماذج» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Learn Rust Coding، انتقل إلى CoddyKit PRO. تتضمن دورة Learn Rust Coding 4 دروس في المجموع.
ماذا ستتعلم في «نقاط النهاية والنماذج»؟
المسارات والبيانات تتمرن على Learn Rust Coding مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Learn Rust Coding؟
لا تُشترط خبرة سابقة. Learn Rust Coding على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «نقاط النهاية والنماذج»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Learn Rust Coding هذا؟
نعم. كل درس في Learn Rust Coding يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- إعداد المشروع
- نقاط النهاية والنماذج
- تكامل قاعدة البيانات
- اختبار API