TP3.1 - الهندسة الطبقية وAPI CRUD كاملة
تطبيق إدارة الطلاب
الأهداف
سيمكنك هذا العمل العملي من:
- فهم الهندسة الطبقية (Controller → Service → Repository)
- إنشاء API REST CRUD كاملة
- تنفيذ عمليات Create, Read, Update, Delete
- استخدام Spring Data JPA مع H2 (قاعدة بيانات في الذاكرة)
- اختبار API REST باستخدام Postman أو متصفح
- إدارة الأخطاء والتحقق من الصحة
جدول المحتويات
- الهندسة الطبقية
- إنشاء المشروع
- إنشاء الكيان Student
- إنشاء الـ Repository
- إنشاء الـ Service
- إنشاء الـ Controller
- إعداد H2
- اختبارات الـ Endpoints
- تمارين إضافية
الموارد
التوثيق مع لقطات شاشة مفصلة:
1. الهندسة الطبقية
نظرة عامة
تفصل الهندسة الطبقية المسؤوليات إلى 4 مستويات مختلفة:
+----------------+ +----------------+ +----------------+ +----------------+
| | | | | | | |
| Controller +------>+ Service +------>+ Repository +------>+ Database |
| | | | | | | |
+----------------+ +----------------+ +----------------+ +----------------+
(HTTP) (منطق الأعمال) (الوصول إلى البيانات) (التخزين)
مخطط Mermaid
مسؤوليات كل طبقة
1. Controller (طبقة العرض)
الدور:
- نقطة الدخول للتطبيق
- استقبال الطلبات HTTP (GET, POST, PUT, DELETE)
- تحليل معلمات الطلب
- استدعاء الـ Service المناسب
- إرجاع الرد إلى العميل (JSON)
التعليقات التوضيحية:
@RestController@RequestMapping@GetMapping,@PostMapping,@PutMapping,@DeleteMapping
مثال:
@RestController
@RequestMapping("/api/students")
public class StudentController {
@Autowired
private StudentService studentService;
@GetMapping
public List<Student> getAllStudents() {
return studentService.getAllStudents();
}
}
2. Service (طبقة الأعمال)
الدور:
- يحتوي على منطق الأعمال للتطبيق
- وسيط بين Controller وRepository
- تطبيق قواعد التحقق
- إجراء الحسابات والتحويلات
- إدارة المعاملات
التعليقات التوضيحية:
@Service@Transactional(للمعاملات)
مثال:
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
public List<Student> getAllStudents() {
return studentRepository.findAll();
}
}
3. Repository (طبقة الوصول إلى البيانات)
الدور:
- التفاعل مباشرة مع قاعدة البيانات
- إجراء عمليات CRUD
- استخدام Spring Data JPA
- توليد تلقائي لطلبات SQL
التعليقات التوضيحية:
@Repository- واجهة تمتد
JpaRepository
مثال:
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
// طرق يتم توليدها تلقائيًا بواسطة Spring Data JPA
}
4. Database (طبقة الاستمرارية)
الدور:
- التخزين الفعلي للبيانات
- إدارة معاملات ACID
- الفهرسة والتحسين
التقنيات:
- H2 (قاعدة بيانات في الذاكرة للتطوير/الاختبارات)
- PostgreSQL, MySQL (للإنتاج)
تدفق الطلب
2. إنشاء المشروع
2.1 Spring Initializr
اذهب إلى https://start.spring.io/
الإعدادات:
| المعلمة | القيمة |
|---|---|
| Project | Maven |
| Language | Java |
| Spring Boot | 3.2.0 (أو أحدث إصدار مستقر) |
| Group | com.example |
| Artifact | student-management |
| Name | student-management |
| Package name | com.example.student |
| Packaging | Jar |
| Java | 17 |
الإضافات التي يجب إضافتها:
- Spring Web - لإنشاء API REST
- Spring Data JPA - للاستمرارية
- H2 Database - قاعدة بيانات في الذاكرة
- Validation - للتحقق من صحة البيانات
- Spring Boot DevTools - لإعادة التحميل التلقائي
2.2 الاستيراد في VS Code
- قم بتنزيل المشروع (ZIP)
- فك الضغط في مساحة العمل الخاصة بك
- افتح VS Code
- File → Open Folder → اختر
student-management - انتظر حتى يقوم Maven بتنزيل الإضافات
2.3 هيكل المشروع
قم بإنشاء هيكل الحزم التالي:
src/main/java/com/example/student/
├── StudentManagementApplication.java
├── controller/
│ └── StudentController.java
├── service/
│ └── StudentService.java
├── repository/
│ └── StudentRepository.java
├── model/
│ └── Student.java
└── exception/
└── ResourceNotFoundException.java
3. إنشاء الكيان Student
3.1 فئة Student
قم بإنشاء src/main/java/com/example/student/model/Student.java:
package com.example.student.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "الاسم الأول مطلوب")
@Size(min = 2, max = 50, message = "يجب أن يحتوي الاسم الأول على 2 إلى 50 حرفًا")
@Column(name = "first_name", nullable = false)
private String firstName;
@NotBlank(message = "الاسم مطلوب")
@Size(min = 2, max = 50, message = "يجب أن يحتوي الاسم على 2 إلى 50 حرفًا")
@Column(name = "last_name", nullable = false)
private String lastName;
@NotBlank(message = "البريد الإلكتروني مطلوب")
@Email(message = "تنسيق البريد الإلكتروني غير صالح")
@Column(name = "email", nullable = false, unique = true)
private String email;
@Column(name = "age")
private Integer age;
// منشئ افتراضي (مطلوب بواسطة JPA)
public Student() {
}
// منشئ مع المعلمات
public Student(String firstName, String lastName, String email, Integer age) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.age = age;
}
// Getters وSetters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", age=" + age +
'}';
}
}
3.2 شرح التعليقات التوضيحية
| التعليق التوضيحي | الدور |
|---|---|
@Entity | تحديد الفئة ككيان JPA |
@Table(name = "students") | اسم الجدول SQL |
@Id | المفتاح الأساسي |
@GeneratedValue(strategy = GenerationType.IDENTITY) | الترقيم التلقائي |
@Column | إعداد العمود |
@NotBlank | التحقق: الحقل غير فارغ |
@Size | التحقق: الحجم الأدنى/الأقصى |
@Email | التحقق: تنسيق البريد الإلكتروني |
4. إنشاء الـ Repository
4.1 واجهة StudentRepository
قم بإنشاء src/main/java/com/example/student/repository/StudentRepository.java:
package com.example.student.repository;
import com.example.student.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
// Spring Data JPA تولد التنفيذ تلقائيًا
// طرق مخصصة (اختيارية)
Optional<Student> findByEmail(String email);
boolean existsByEmail(String email);
}
4.2 الطرق المتاحة
الطرق الموروثة من JpaRepository:
| الطريقة | الوصف |
|---|---|
save(Student s) | حفظ أو تحديث |
findAll() | استرجاع جميع الطلاب |
findById(Long id) | استرجاع بواسطة ID |
deleteById(Long id) | حذف بواسطة ID |
count() | عد السجلات |
existsById(Long id) | التحقق من الوجود |
الطرق المخصصة:
findByEmail(String email)- البحث بواسطة البريد الإلكترونيexistsByEmail(String email)- التحقق مما إذا كان البريد الإلكتروني موجودًا
ملاحظة: Spring Data JPA تولد تلقائيًا طلبات SQL بناءً على اسم الطريقة!
5. إنشاء الـ Service
5.1 فئة StudentService
قم بإنشاء src/main/java/com/example/student/service/StudentService.java:
package com.example.student.service;
import com.example.student.model.Student;
import com.example.student.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
// CREATE - إنشاء طالب
public Student createStudent(Student student) {
// التحقق مما إذا كان البريد الإلكتروني موجودًا بالفعل
if (studentRepository.existsByEmail(student.getEmail())) {
throw new RuntimeException("طالب بهذا البريد الإلكتروني موجود بالفعل");
}
return studentRepository.save(student);
}
// READ - استرجاع جميع الطلاب
public List<Student> getAllStudents() {
return studentRepository.findAll();
}
// READ - استرجاع طالب بواسطة ID
public Optional<Student> getStudentById(Long id) {
return studentRepository.findById(id);
}
// UPDATE - تحديث طالب
public Student updateStudent(Long id, Student studentDetails) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("طالب غير موجود بالمعرف: " + id));
student.setFirstName(studentDetails.getFirstName());
student.setLastName(studentDetails.getLastName());
student.setEmail(studentDetails.getEmail());
student.setAge(studentDetails.getAge());
return studentRepository.save(student);
}
// DELETE - حذف طالب
public void deleteStudent(Long id) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("طالب غير موجود بالمعرف: " + id));
studentRepository.delete(student);
}
// طريقة مساعدة - البحث بواسطة البريد الإلكتروني
public Optional<Student> getStudentByEmail(String email) {
return studentRepository.findByEmail(email);
}
}
5.2 منطق الأعمال
يحتوي الـ Service على:
- التحقق: التحقق مما إذا كان البريد الإلكتروني موجودًا قبل الإنشاء
- إدارة الأخطاء: إطلاق استثناءات إذا لم يتم العثور على الطالب
- التنسيق: تنسيق الاستدعاءات إلى الـ Repository
- التحويل: تحويل البيانات إذا لزم الأمر
6. إنشاء الـ Controller
6.1 فئة StudentController
قم بإنشاء src/main/java/com/example/student/controller/StudentController.java:
package com.example.student.controller;
import com.example.student.model.Student;
import com.example.student.service.StudentService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/students")
public class StudentController {
@Autowired
private StudentService studentService;
// CREATE - POST /api/students
@PostMapping
public ResponseEntity<Student> createStudent(@Valid @RequestBody Student student) {
try {
Student createdStudent = studentService.createStudent(student);
return new ResponseEntity<>(createdStudent, HttpStatus.CREATED);
} catch (RuntimeException e) {
return new ResponseEntity<>(null, HttpStatus.CONFLICT);
}
}
// READ ALL - GET /api/students
@GetMapping
public ResponseEntity<List<Student>> getAllStudents() {
List<Student> students = studentService.getAllStudents();
return new ResponseEntity<>(students, HttpStatus.OK);
}
// READ ONE - GET /api/students/{id}
@GetMapping("/{id}")
public ResponseEntity<Student> getStudentById(@PathVariable Long id) {
return studentService.getStudentById(id)
.map(student -> new ResponseEntity<>(student, HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
// UPDATE - PUT /api/students/{id}
@PutMapping("/{id}")
public ResponseEntity<Student> updateStudent(
@PathVariable Long id,
@Valid @RequestBody Student studentDetails) {
try {
Student updatedStudent = studentService.updateStudent(id, studentDetails);
return new ResponseEntity<>(updatedStudent, HttpStatus.OK);
} catch (RuntimeException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
// DELETE - DELETE /api/students/{id}
@DeleteMapping("/{id}")
public ResponseEntity<HttpStatus> deleteStudent(@PathVariable Long id) {
try {
studentService.deleteStudent(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} catch (RuntimeException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
// SEARCH BY EMAIL - GET /api/students/email/{email}
@GetMapping("/email/{email}")
public ResponseEntity<Student> getStudentByEmail(@PathVariable String email) {
return studentService.getStudentByEmail(email)
.map(student -> new ResponseEntity<>(student, HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
}
6.2 Endpoints REST
| طريقة HTTP | Endpoint | الإجراء | رمز النجاح |
|---|---|---|---|
| POST | /api/students | إنشاء طالب | 201 CREATED |
| GET | /api/students | قائمة جميع الطلاب | 200 OK |
| GET | /api/students/\{id\} | استرجاع طالب | 200 OK |
| PUT | /api/students/\{id\} | تحديث طالب | 200 OK |
| DELETE | /api/students/\{id\} | حذف طالب | 204 NO CONTENT |
| GET | /api/students/email/\{email\} | البحث بواسطة البريد الإلكتروني | 200 OK |
6.3 رموز الحالة HTTP
7. إعداد H2
7.1 ملف application.properties
قم بإنشاء/تعديل src/main/resources/application.properties:
# منفذ الخادم
server.port=8080
# إعداد H2 Database
spring.datasource.url=jdbc:h2:mem:studentdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# إعداد JPA
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# وحدة التحكم H2 (متاحة عبر المتصفح)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
7.2 الوصول إلى وحدة التحكم H2
بمجرد بدء التطبيق، قم بالوصول إلى:
http://localhost:8080/h2-console
إعدادات الاتصال:
| الحقل | القيمة |
|---|---|
| JDBC URL | jdbc:h2:mem:studentdb |
| User Name | sa |
| Password | (اتركه فارغًا) |
8. اختبارات الـ Endpoints
8.1 بدء التطبيق
في VS Code:
mvn clean install -DskipTests spring-boot:run
السجلات المتوقعة:
Hibernate: create table students (...)
Started StudentManagementApplication in X.XXX seconds
8.2 اختبارات باستخدام المتصفح
اختبار 1: إنشاء طالب (POST)
ملاحظة: المتصفح لا يمكنه القيام بـ POST بسهولة. استخدم Postman أو curl.
باستخدام curl (PowerShell/CMD):
curl -X POST http://localhost:8080/api/students ^
-H "Content-Type: application/json" ^
-d "{\"firstName\":\"Jean\",\"lastName\":\"Dupont\",\"email\":\"jean.dupont@example.com\",\"age\":20}"
اختبار 2: قائمة جميع الطلاب (GET)
في المتصفح:
http://localhost:8080/api/students
الاستجابة المتوقعة:
[
{
"id": 1,
"firstName": "Jean",
"lastName": "Dupont",
"email": "jean.dupont@example.com",
"age": 20
}
]
اختبار 3: استرجاع طالب بواسطة ID (GET)
http://localhost:8080/api/students/1
الاستجابة المتوقعة:
{
"id": 1,
"firstName": "Jean",
"lastName": "Dupont",
"email": "jean.dupont@example.com",
"age": 20
}
8.3 اختبارات باستخدام Postman
تثبيت Postman
- قم بتنزيل Postman: https://www.postman.com/downloads/
- قم بتثبيت وتشغيل Postman
مجموعة الاختبارات
اختبار 1: CREATE (POST)
- الطريقة: POST
- URL:
http://localhost:8080/api/students - Headers:
Content-Type: application/json - Body (raw JSON):
{
"firstName": "Alice",
"lastName": "Martin",
"email": "alice.martin@example.com",
"age": 22
}
النتيجة المتوقعة: حالة 201 CREATED
اختبار 2: READ ALL (GET)
- الطريقة: GET
- URL:
http://localhost:8080/api/students
النتيجة المتوقعة: حالة 200 OK + قائمة الطلاب
اختبار 3: READ ONE (GET)
- الطريقة: GET
- URL:
http://localhost:8080/api/students/1
النتيجة المتوقعة: حالة 200 OK + تفاصيل الطالب
اختبار 4: UPDATE (PUT)
- الطريقة: PUT
- URL:
http://localhost:8080/api/students/1 - Headers:
Content-Type: application/json - Body (raw JSON):
{
"firstName": "Alice",
"lastName": "Martin-Modifié",
"email": "alice.martin.updated@example.com",
"age": 23
}
النتيجة المتوقعة: حالة 200 OK + طالب محدث
اختبار 5: DELETE (DELETE)
- الطريقة: DELETE
- URL:
http://localhost:8080/api/students/1
النتيجة المتوقعة: حالة 204 NO CONTENT
اختبار 6: SEARCH BY EMAIL (GET)
- الطريقة: GET
- URL:
http://localhost:8080/api/students/email/alice.martin@example.com
النتيجة المتوقعة: حالة 200 OK + طالب موجود
8.4 التحقق في وحدة التحكم H2
- قم بالوصول إلى
http://localhost:8080/h2-console - قم بتسجيل الدخول
- نفذ الطلب:
SELECT * FROM students;
يجب أن ترى جميع الطلاب الذين تم إنشاؤهم.
9. تمارين إضافية
تمرين 1: إضافة حقل "برنامج"
الهدف: إضافة حقل program (String) إلى الكيان Student
التعليمات:
- قم بتعديل الفئة
Student.java - أضف الحقل
program - قم بإنشاء getters/setters
- اختبر إنشاء طالب مع برنامج
تمرين 2: البحث بواسطة الاسم
الهدف: تنفيذ البحث بواسطة اسم العائلة
التعليمات:
- أضف طريقة في
StudentRepository:List<Student> findByLastName(String lastName); - أضف طريقة في
StudentService - أضف endpoint في
StudentController:@GetMapping("/lastname/\{lastName\}") - اختبر باستخدام Postman
تمرين 3: الترقيم
الهدف: إضافة الترقيم إلى قائمة الطلاب
التعليمات:
- قم بتعديل الطريقة
getAllStudents()في الـ Service:public Page<Student> getAllStudents(int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return studentRepository.findAll(pageable);
} - قم بتعديل الـ Controller لقبول المعلمات
pageوsize - اختبر:
http://localhost:8080/api/students?page=0&size=5
تمرين 4: إدارة الأخطاء المخصصة
الهدف: إنشاء إدارة أخطاء شاملة
التعليمات:
- قم بإنشاء
ResourceNotFoundException.java:
package com.example.student.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
- قم بإنشاء
GlobalExceptionHandler.java:
package com.example.student.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleResourceNotFound(ResourceNotFoundException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
- استخدم
ResourceNotFoundExceptionفي الـ Service - اختبر بمحاولة الوصول إلى طالب غير موجود
تمرين 5: التحقق المتقدم
الهدف: إضافة التحقق المخصص
التعليمات:
- أضف قيود في
Student.java:@Min(value = 18, message = "الحد الأدنى للعمر هو 18 عامًا")
@Max(value = 100, message = "الحد الأقصى للعمر هو 100 عامًا")
private Integer age; - اختبر بإنشاء طالب بعمر غير صالح
- لاحظ الرمز 400 BAD REQUEST
تمرين 6: الانتقال إلى PostgreSQL
الهدف: استبدال H2 بـ PostgreSQL
التعليمات:
- أضف اعتماد PostgreSQL في
pom.xml - قم بتعديل
application.properties:spring.datasource.url=jdbc:postgresql://localhost:5432/studentdb
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update - قم بإنشاء قاعدة البيانات
studentdbفي PostgreSQL - أعد تشغيل التطبيق
- تحقق من أن البيانات تستمر بعد إعادة التشغيل
حل المشاكل
مشكلة 1: خطأ 404 على جميع الـ endpoints
السبب: الـ Controller غير مفحوص بواسطة Spring
الحل:
تحقق من أن StudentController في نفس الحزمة (أو حزمة فرعية) لـ StudentManagementApplication.
مشكلة 2: خطأ في التحقق
الأعراض:
Validation failed for object='student'
الحل:
تحقق من أن البيانات المرسلة تلتزم بالقيود @NotBlank, @Email, @Size, إلخ.
مشكلة 3: وحدة التحكم H2 غير متاحة
الأعراض:
404 on /h2-console
الحل:
تحقق في application.properties:
spring.h2.console.enabled=true
مشكلة 4: تعارض البريد الإلكتروني
الأعراض:
Status 409 CONFLICT
الحل:
طبيعي! طالب بهذا البريد الإلكتروني موجود بالفعل. هذا هو التحقق الذي يعمل.
أسئلة المراجعة
-
ما هي مسؤولية كل طبقة (Controller, Service, Repository)؟
-
لماذا نفصل الكود إلى طبقات؟
-
ما الفرق بين
@RestControllerو@Service؟ -
ماذا تفعل التعليمة
@Validفي الـ Controller؟ -
كيف يقوم Spring Data JPA بتوليد طلبات SQL؟
-
ما الفرق بين
save()للإنشاء والتحديث؟ -
لماذا نستخدم
Optional<Student>في الـ Service؟ -
ما هو رمز HTTP الذي يجب إرجاعه بعد حذف ناجح؟
التسليمات
يجب تسليم:
- الكود المصدري الكامل للمشروع
- لقطات شاشة Postman تظهر 6 اختبارات ناجحة
- لقطة شاشة وحدة التحكم H2 مع البيانات
- على الأقل 2 من التمارين المكتملة
- وثيقة إجابات لأسئلة المراجعة
معايير التقييم
| المعيار | النقاط |
|---|---|
| الهندسة الطبقية مطبقة بشكل صحيح | 20% |
| كيان Student مع التحقق | 15% |
| Repository وظيفي | 10% |
| Service مع منطق الأعمال | 20% |
| Controller مع جميع الـ endpoints CRUD | 20% |
| اختبارات ناجحة (Postman/Browser) | 10% |
| تمارين إضافية | 5% |
| المجموع | 100% |
الملخص
ما تعلمته
- الهندسة الطبقية (Controller → Service → Repository → Database)
- إنشاء API REST CRUD كاملة
- عمليات CRUD: Create, Read, Update, Delete
- التحقق من البيانات باستخدام Bean Validation
- إدارة رموز الحالة HTTP
- استخدام H2 للتطوير
- اختبارات API باستخدام Postman
الممارسات الجيدة
- فصل المسؤوليات في طبقات مختلفة
- التحقق من البيانات عند الإدخال (Controller)
- إدارة الأخطاء بشكل صحيح (استثناءات مخصصة)
- استخدام رموز HTTP المناسبة
- اختبار جميع الـ endpoints
- توثيق الـ API
الخطوات التالية
في الأعمال العملية القادمة، سنرى:
- TP4: الأمان باستخدام Spring Security
- TP5: التوثيق باستخدام Swagger/OpenAPI
- TP6: النشر في الإنتاج