schemantic 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/LICENSE +21 -0
  3. package/README.md +827 -0
  4. package/dist/__tests__/setup.d.ts +6 -0
  5. package/dist/__tests__/setup.d.ts.map +1 -0
  6. package/dist/__tests__/setup.js +30 -0
  7. package/dist/__tests__/setup.js.map +1 -0
  8. package/dist/__tests__/test-config.d.ts +36 -0
  9. package/dist/__tests__/test-config.d.ts.map +1 -0
  10. package/dist/__tests__/test-config.js +563 -0
  11. package/dist/__tests__/test-config.js.map +1 -0
  12. package/dist/cli/index.d.ts +118 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +657 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/core/index.d.ts +5 -0
  17. package/dist/core/index.d.ts.map +1 -0
  18. package/dist/core/index.js +21 -0
  19. package/dist/core/index.js.map +1 -0
  20. package/dist/core/schemantic.d.ts +95 -0
  21. package/dist/core/schemantic.d.ts.map +1 -0
  22. package/dist/core/schemantic.js +543 -0
  23. package/dist/core/schemantic.js.map +1 -0
  24. package/dist/generators/api-client-generator.d.ts +145 -0
  25. package/dist/generators/api-client-generator.d.ts.map +1 -0
  26. package/dist/generators/api-client-generator.js +856 -0
  27. package/dist/generators/api-client-generator.js.map +1 -0
  28. package/dist/generators/base.d.ts +117 -0
  29. package/dist/generators/base.d.ts.map +1 -0
  30. package/dist/generators/base.js +232 -0
  31. package/dist/generators/base.js.map +1 -0
  32. package/dist/generators/enum-generator.d.ts +52 -0
  33. package/dist/generators/enum-generator.d.ts.map +1 -0
  34. package/dist/generators/enum-generator.js +178 -0
  35. package/dist/generators/enum-generator.js.map +1 -0
  36. package/dist/generators/factory.d.ts +59 -0
  37. package/dist/generators/factory.d.ts.map +1 -0
  38. package/dist/generators/factory.js +127 -0
  39. package/dist/generators/factory.js.map +1 -0
  40. package/dist/generators/hook-generator.d.ts +36 -0
  41. package/dist/generators/hook-generator.d.ts.map +1 -0
  42. package/dist/generators/hook-generator.js +495 -0
  43. package/dist/generators/hook-generator.js.map +1 -0
  44. package/dist/generators/index.d.ts +10 -0
  45. package/dist/generators/index.d.ts.map +1 -0
  46. package/dist/generators/index.js +26 -0
  47. package/dist/generators/index.js.map +1 -0
  48. package/dist/generators/object-generator.d.ts +48 -0
  49. package/dist/generators/object-generator.d.ts.map +1 -0
  50. package/dist/generators/object-generator.js +280 -0
  51. package/dist/generators/object-generator.js.map +1 -0
  52. package/dist/generators/primitive-generator.d.ts +40 -0
  53. package/dist/generators/primitive-generator.d.ts.map +1 -0
  54. package/dist/generators/primitive-generator.js +150 -0
  55. package/dist/generators/primitive-generator.js.map +1 -0
  56. package/dist/index.d.ts +17 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +42 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/parsers/base.d.ts +101 -0
  61. package/dist/parsers/base.d.ts.map +1 -0
  62. package/dist/parsers/base.js +77 -0
  63. package/dist/parsers/base.js.map +1 -0
  64. package/dist/parsers/factory.d.ts +64 -0
  65. package/dist/parsers/factory.d.ts.map +1 -0
  66. package/dist/parsers/factory.js +116 -0
  67. package/dist/parsers/factory.js.map +1 -0
  68. package/dist/parsers/index.d.ts +7 -0
  69. package/dist/parsers/index.d.ts.map +1 -0
  70. package/dist/parsers/index.js +23 -0
  71. package/dist/parsers/index.js.map +1 -0
  72. package/dist/parsers/openapi.d.ts +52 -0
  73. package/dist/parsers/openapi.d.ts.map +1 -0
  74. package/dist/parsers/openapi.js +396 -0
  75. package/dist/parsers/openapi.js.map +1 -0
  76. package/dist/plugins/branded-types.d.ts +28 -0
  77. package/dist/plugins/branded-types.d.ts.map +1 -0
  78. package/dist/plugins/branded-types.js +568 -0
  79. package/dist/plugins/branded-types.js.map +1 -0
  80. package/dist/plugins/builtin.d.ts +38 -0
  81. package/dist/plugins/builtin.d.ts.map +1 -0
  82. package/dist/plugins/builtin.js +283 -0
  83. package/dist/plugins/builtin.js.map +1 -0
  84. package/dist/plugins/index.d.ts +7 -0
  85. package/dist/plugins/index.d.ts.map +1 -0
  86. package/dist/plugins/index.js +23 -0
  87. package/dist/plugins/index.js.map +1 -0
  88. package/dist/plugins/loader.d.ts +92 -0
  89. package/dist/plugins/loader.d.ts.map +1 -0
  90. package/dist/plugins/loader.js +245 -0
  91. package/dist/plugins/loader.js.map +1 -0
  92. package/dist/plugins/manager.d.ts +99 -0
  93. package/dist/plugins/manager.d.ts.map +1 -0
  94. package/dist/plugins/manager.js +233 -0
  95. package/dist/plugins/manager.js.map +1 -0
  96. package/dist/plugins/performance-monitoring.d.ts +98 -0
  97. package/dist/plugins/performance-monitoring.d.ts.map +1 -0
  98. package/dist/plugins/performance-monitoring.js +596 -0
  99. package/dist/plugins/performance-monitoring.js.map +1 -0
  100. package/dist/plugins/request-deduplication.d.ts +27 -0
  101. package/dist/plugins/request-deduplication.d.ts.map +1 -0
  102. package/dist/plugins/request-deduplication.js +650 -0
  103. package/dist/plugins/request-deduplication.js.map +1 -0
  104. package/dist/plugins/zod-validation.d.ts +63 -0
  105. package/dist/plugins/zod-validation.d.ts.map +1 -0
  106. package/dist/plugins/zod-validation.js +743 -0
  107. package/dist/plugins/zod-validation.js.map +1 -0
  108. package/dist/types/core.d.ts +203 -0
  109. package/dist/types/core.d.ts.map +1 -0
  110. package/dist/types/core.js +24 -0
  111. package/dist/types/core.js.map +1 -0
  112. package/dist/types/index.d.ts +7 -0
  113. package/dist/types/index.d.ts.map +1 -0
  114. package/dist/types/index.js +23 -0
  115. package/dist/types/index.js.map +1 -0
  116. package/dist/types/openapi.d.ts +250 -0
  117. package/dist/types/openapi.d.ts.map +1 -0
  118. package/dist/types/openapi.js +8 -0
  119. package/dist/types/openapi.js.map +1 -0
  120. package/dist/types/schema.d.ts +68 -0
  121. package/dist/types/schema.d.ts.map +1 -0
  122. package/dist/types/schema.js +22 -0
  123. package/dist/types/schema.js.map +1 -0
  124. package/docs/ADVANCED_PLUGINS_IMPLEMENTATION.md +355 -0
  125. package/docs/ADVANCED_PLUGIN_DEVELOPMENT.md +309 -0
  126. package/docs/API.md +600 -0
  127. package/docs/CONFIGURATION.md +722 -0
  128. package/docs/FASTAPI_TUTORIAL.md +1497 -0
  129. package/docs/PLUGINS.md +683 -0
  130. package/docs/PLUGIN_DEVELOPMENT.md +720 -0
  131. package/package.json +90 -0
@@ -0,0 +1,1497 @@
1
+ # FastAPI + Schemantic: Complete Tutorial
2
+
3
+ This tutorial shows you how to build a complete, type-safe full-stack application using FastAPI, Schemantic, and React with TypeScript.
4
+
5
+ ## Prerequisites
6
+
7
+ - Python 3.8+ with pip
8
+ - Node.js 16+ with npm
9
+ - Basic knowledge of FastAPI and React
10
+
11
+ ## Project Overview
12
+
13
+ We'll build an e-commerce API with the following features:
14
+
15
+ - User authentication and management
16
+ - Product catalog
17
+ - Order management
18
+ - Type-safe frontend with generated API client
19
+ - React hooks for data fetching
20
+
21
+ ## Part 1: FastAPI Backend Setup
22
+
23
+ ### 1.1 Create the FastAPI Project
24
+
25
+ ```bash
26
+ # Create project directory
27
+ mkdir fastapi-Schemantic-demo
28
+ cd fastapi-Schemantic-demo
29
+
30
+ # Create backend directory
31
+ mkdir backend
32
+ cd backend
33
+
34
+ # Create virtual environment
35
+ python -m venv venv
36
+ source venv/bin/activate # On Windows: venv\Scripts\activate
37
+
38
+ # Install dependencies
39
+ pip install fastapi uvicorn pydantic sqlalchemy python-jose[cryptography] passlib[bcrypt] python-multipart
40
+ ```
41
+
42
+ ### 1.2 Create the Database Models
43
+
44
+ Create `backend/models.py`:
45
+
46
+ ```python
47
+ from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Boolean
48
+ from sqlalchemy.ext.declarative import declarative_base
49
+ from sqlalchemy.orm import relationship
50
+ from datetime import datetime
51
+
52
+ Base = declarative_base()
53
+
54
+ class User(Base):
55
+ __tablename__ = "users"
56
+
57
+ id = Column(Integer, primary_key=True, index=True)
58
+ email = Column(String, unique=True, index=True, nullable=False)
59
+ username = Column(String, unique=True, index=True, nullable=False)
60
+ hashed_password = Column(String, nullable=False)
61
+ full_name = Column(String)
62
+ is_active = Column(Boolean, default=True)
63
+ is_admin = Column(Boolean, default=False)
64
+ created_at = Column(DateTime, default=datetime.utcnow)
65
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
66
+
67
+ # Relationships
68
+ orders = relationship("Order", back_populates="user")
69
+
70
+ class Product(Base):
71
+ __tablename__ = "products"
72
+
73
+ id = Column(Integer, primary_key=True, index=True)
74
+ name = Column(String, nullable=False)
75
+ description = Column(String)
76
+ price = Column(Float, nullable=False)
77
+ category = Column(String, nullable=False)
78
+ stock_quantity = Column(Integer, default=0)
79
+ image_url = Column(String)
80
+ is_active = Column(Boolean, default=True)
81
+ created_at = Column(DateTime, default=datetime.utcnow)
82
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
83
+
84
+ # Relationships
85
+ order_items = relationship("OrderItem", back_populates="product")
86
+
87
+ class Order(Base):
88
+ __tablename__ = "orders"
89
+
90
+ id = Column(Integer, primary_key=True, index=True)
91
+ user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
92
+ total_amount = Column(Float, nullable=False)
93
+ status = Column(String, default="pending") # pending, processing, shipped, delivered, cancelled
94
+ shipping_address = Column(String, nullable=False)
95
+ created_at = Column(DateTime, default=datetime.utcnow)
96
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
97
+
98
+ # Relationships
99
+ user = relationship("User", back_populates="orders")
100
+ items = relationship("OrderItem", back_populates="order")
101
+
102
+ class OrderItem(Base):
103
+ __tablename__ = "order_items"
104
+
105
+ id = Column(Integer, primary_key=True, index=True)
106
+ order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
107
+ product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
108
+ quantity = Column(Integer, nullable=False)
109
+ unit_price = Column(Float, nullable=False)
110
+
111
+ # Relationships
112
+ order = relationship("Order", back_populates="items")
113
+ product = relationship("Product", back_populates="order_items")
114
+ ```
115
+
116
+ ### 1.3 Create Pydantic Schemas
117
+
118
+ Create `backend/schemas.py`:
119
+
120
+ ```python
121
+ from pydantic import BaseModel, EmailStr, Field
122
+ from datetime import datetime
123
+ from typing import List, Optional
124
+ from enum import Enum
125
+
126
+ # Enums
127
+ class OrderStatus(str, Enum):
128
+ pending = "pending"
129
+ processing = "processing"
130
+ shipped = "shipped"
131
+ delivered = "delivered"
132
+ cancelled = "cancelled"
133
+
134
+ class ProductCategory(str, Enum):
135
+ electronics = "electronics"
136
+ clothing = "clothing"
137
+ books = "books"
138
+ home = "home"
139
+ sports = "sports"
140
+
141
+ # Base schemas
142
+ class UserBase(BaseModel):
143
+ email: EmailStr = Field(..., description="User's email address")
144
+ username: str = Field(..., min_length=3, max_length=50, description="Unique username")
145
+ full_name: Optional[str] = Field(None, max_length=100, description="User's full name")
146
+
147
+ class UserCreate(UserBase):
148
+ password: str = Field(..., min_length=8, description="User's password (min 8 characters)")
149
+
150
+ class UserUpdate(BaseModel):
151
+ email: Optional[EmailStr] = Field(None, description="Updated email address")
152
+ username: Optional[str] = Field(None, min_length=3, max_length=50, description="Updated username")
153
+ full_name: Optional[str] = Field(None, max_length=100, description="Updated full name")
154
+ is_active: Optional[bool] = Field(None, description="Whether user is active")
155
+
156
+ class User(UserBase):
157
+ id: int = Field(..., description="Unique user identifier")
158
+ is_active: bool = Field(..., description="Whether user is active")
159
+ is_admin: bool = Field(..., description="Whether user has admin privileges")
160
+ created_at: datetime = Field(..., description="User creation timestamp")
161
+ updated_at: datetime = Field(..., description="Last update timestamp")
162
+
163
+ class Config:
164
+ from_attributes = True
165
+
166
+ # Product schemas
167
+ class ProductBase(BaseModel):
168
+ name: str = Field(..., min_length=1, max_length=200, description="Product name")
169
+ description: Optional[str] = Field(None, max_length=1000, description="Product description")
170
+ price: float = Field(..., gt=0, description="Product price (must be positive)")
171
+ category: ProductCategory = Field(..., description="Product category")
172
+ stock_quantity: int = Field(0, ge=0, description="Available stock quantity")
173
+ image_url: Optional[str] = Field(None, description="Product image URL")
174
+
175
+ class ProductCreate(ProductBase):
176
+ pass
177
+
178
+ class ProductUpdate(BaseModel):
179
+ name: Optional[str] = Field(None, min_length=1, max_length=200, description="Updated product name")
180
+ description: Optional[str] = Field(None, max_length=1000, description="Updated description")
181
+ price: Optional[float] = Field(None, gt=0, description="Updated price")
182
+ category: Optional[ProductCategory] = Field(None, description="Updated category")
183
+ stock_quantity: Optional[int] = Field(None, ge=0, description="Updated stock quantity")
184
+ image_url: Optional[str] = Field(None, description="Updated image URL")
185
+ is_active: Optional[bool] = Field(None, description="Whether product is active")
186
+
187
+ class Product(ProductBase):
188
+ id: int = Field(..., description="Unique product identifier")
189
+ is_active: bool = Field(..., description="Whether product is active")
190
+ created_at: datetime = Field(..., description="Product creation timestamp")
191
+ updated_at: datetime = Field(..., description="Last update timestamp")
192
+
193
+ class Config:
194
+ from_attributes = True
195
+
196
+ # Order schemas
197
+ class OrderItemBase(BaseModel):
198
+ product_id: int = Field(..., description="Product identifier")
199
+ quantity: int = Field(..., gt=0, description="Quantity ordered")
200
+
201
+ class OrderItemCreate(OrderItemBase):
202
+ pass
203
+
204
+ class OrderItem(OrderItemBase):
205
+ id: int = Field(..., description="Unique order item identifier")
206
+ unit_price: float = Field(..., description="Price per unit at time of order")
207
+ product: Product = Field(..., description="Product details")
208
+
209
+ class Config:
210
+ from_attributes = True
211
+
212
+ class OrderBase(BaseModel):
213
+ shipping_address: str = Field(..., min_length=10, max_length=500, description="Shipping address")
214
+
215
+ class OrderCreate(OrderBase):
216
+ items: List[OrderItemCreate] = Field(..., min_items=1, description="List of items to order")
217
+
218
+ class OrderUpdate(BaseModel):
219
+ status: Optional[OrderStatus] = Field(None, description="Updated order status")
220
+ shipping_address: Optional[str] = Field(None, min_length=10, max_length=500, description="Updated shipping address")
221
+
222
+ class Order(OrderBase):
223
+ id: int = Field(..., description="Unique order identifier")
224
+ user_id: int = Field(..., description="User who placed the order")
225
+ total_amount: float = Field(..., description="Total order amount")
226
+ status: OrderStatus = Field(..., description="Current order status")
227
+ created_at: datetime = Field(..., description="Order creation timestamp")
228
+ updated_at: datetime = Field(..., description="Last update timestamp")
229
+ items: List[OrderItem] = Field(..., description="List of ordered items")
230
+ user: User = Field(..., description="User details")
231
+
232
+ class Config:
233
+ from_attributes = True
234
+
235
+ # Authentication schemas
236
+ class Token(BaseModel):
237
+ access_token: str = Field(..., description="JWT access token")
238
+ token_type: str = Field("bearer", description="Token type")
239
+
240
+ class TokenData(BaseModel):
241
+ username: Optional[str] = None
242
+
243
+ class LoginRequest(BaseModel):
244
+ username: str = Field(..., description="Username or email")
245
+ password: str = Field(..., description="User password")
246
+
247
+ # API Response schemas
248
+ class APIResponse(BaseModel):
249
+ success: bool = Field(..., description="Whether the operation was successful")
250
+ message: str = Field(..., description="Response message")
251
+ data: Optional[dict] = Field(None, description="Response data")
252
+
253
+ class PaginatedResponse(BaseModel):
254
+ items: List[dict] = Field(..., description="List of items")
255
+ total: int = Field(..., description="Total number of items")
256
+ page: int = Field(..., description="Current page number")
257
+ per_page: int = Field(..., description="Items per page")
258
+ pages: int = Field(..., description="Total number of pages")
259
+
260
+ # Health check schema
261
+ class HealthCheck(BaseModel):
262
+ status: str = Field("healthy", description="Service health status")
263
+ timestamp: datetime = Field(..., description="Health check timestamp")
264
+ version: str = Field("1.0.0", description="API version")
265
+ ```
266
+
267
+ ### 1.4 Create the FastAPI Application
268
+
269
+ Create `backend/main.py`:
270
+
271
+ ```python
272
+ from fastapi import FastAPI, Depends, HTTPException, status
273
+ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
274
+ from fastapi.middleware.cors import CORSMiddleware
275
+ from sqlalchemy.orm import Session
276
+ from typing import List, Optional
277
+ from datetime import datetime, timedelta
278
+ import uvicorn
279
+
280
+ from models import Base, User, Product, Order, OrderItem
281
+ from schemas import (
282
+ User as UserSchema, UserCreate, UserUpdate,
283
+ Product as ProductSchema, ProductCreate, ProductUpdate,
284
+ Order as OrderSchema, OrderCreate, OrderUpdate,
285
+ Token, LoginRequest, HealthCheck, APIResponse, PaginatedResponse,
286
+ OrderStatus, ProductCategory
287
+ )
288
+ from database import engine, get_db
289
+ from auth import authenticate_user, create_access_token, get_current_user, get_password_hash
290
+
291
+ # Create database tables
292
+ Base.metadata.create_all(bind=engine)
293
+
294
+ # FastAPI app configuration
295
+ app = FastAPI(
296
+ title="E-Commerce API",
297
+ description="A comprehensive e-commerce API built with FastAPI and Schemantic",
298
+ version="1.0.0",
299
+ contact={
300
+ "name": "API Support",
301
+ "email": "support@ecommerce-api.com",
302
+ },
303
+ license_info={
304
+ "name": "MIT",
305
+ "url": "https://opensource.org/licenses/MIT",
306
+ },
307
+ )
308
+
309
+ # CORS middleware
310
+ app.add_middleware(
311
+ CORSMiddleware,
312
+ allow_origins=["http://localhost:3000", "http://localhost:5173"], # React dev servers
313
+ allow_credentials=True,
314
+ allow_methods=["*"],
315
+ allow_headers=["*"],
316
+ )
317
+
318
+ # OAuth2 scheme
319
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
320
+
321
+ # Health check endpoint
322
+ @app.get("/health", response_model=HealthCheck, tags=["Health"])
323
+ async def health_check():
324
+ """Check the health status of the API"""
325
+ return HealthCheck(timestamp=datetime.utcnow())
326
+
327
+ # Authentication endpoints
328
+ @app.post("/auth/register", response_model=UserSchema, tags=["Authentication"])
329
+ async def register_user(user: UserCreate, db: Session = Depends(get_db)):
330
+ """Register a new user account"""
331
+ # Check if user already exists
332
+ db_user = db.query(User).filter(
333
+ (User.email == user.email) | (User.username == user.username)
334
+ ).first()
335
+
336
+ if db_user:
337
+ raise HTTPException(
338
+ status_code=400,
339
+ detail="User with this email or username already exists"
340
+ )
341
+
342
+ # Create new user
343
+ hashed_password = get_password_hash(user.password)
344
+ db_user = User(
345
+ email=user.email,
346
+ username=user.username,
347
+ full_name=user.full_name,
348
+ hashed_password=hashed_password
349
+ )
350
+
351
+ db.add(db_user)
352
+ db.commit()
353
+ db.refresh(db_user)
354
+
355
+ return db_user
356
+
357
+ @app.post("/auth/login", response_model=Token, tags=["Authentication"])
358
+ async def login_user(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
359
+ """Authenticate user and return access token"""
360
+ user = authenticate_user(db, form_data.username, form_data.password)
361
+ if not user:
362
+ raise HTTPException(
363
+ status_code=status.HTTP_401_UNAUTHORIZED,
364
+ detail="Incorrect username or password",
365
+ headers={"WWW-Authenticate": "Bearer"},
366
+ )
367
+
368
+ access_token = create_access_token(data={"sub": user.username})
369
+ return {"access_token": access_token, "token_type": "bearer"}
370
+
371
+ @app.get("/auth/me", response_model=UserSchema, tags=["Authentication"])
372
+ async def get_current_user_info(current_user: User = Depends(get_current_user)):
373
+ """Get current authenticated user information"""
374
+ return current_user
375
+
376
+ # User management endpoints
377
+ @app.get("/users", response_model=List[UserSchema], tags=["Users"])
378
+ async def get_users(
379
+ skip: int = 0,
380
+ limit: int = 100,
381
+ current_user: User = Depends(get_current_user),
382
+ db: Session = Depends(get_db)
383
+ ):
384
+ """Get list of users (admin only)"""
385
+ if not current_user.is_admin:
386
+ raise HTTPException(status_code=403, detail="Not enough permissions")
387
+
388
+ users = db.query(User).offset(skip).limit(limit).all()
389
+ return users
390
+
391
+ @app.get("/users/{user_id}", response_model=UserSchema, tags=["Users"])
392
+ async def get_user(
393
+ user_id: int,
394
+ current_user: User = Depends(get_current_user),
395
+ db: Session = Depends(get_db)
396
+ ):
397
+ """Get user by ID"""
398
+ if current_user.id != user_id and not current_user.is_admin:
399
+ raise HTTPException(status_code=403, detail="Not enough permissions")
400
+
401
+ user = db.query(User).filter(User.id == user_id).first()
402
+ if user is None:
403
+ raise HTTPException(status_code=404, detail="User not found")
404
+
405
+ return user
406
+
407
+ @app.put("/users/{user_id}", response_model=UserSchema, tags=["Users"])
408
+ async def update_user(
409
+ user_id: int,
410
+ user_update: UserUpdate,
411
+ current_user: User = Depends(get_current_user),
412
+ db: Session = Depends(get_db)
413
+ ):
414
+ """Update user information"""
415
+ if current_user.id != user_id and not current_user.is_admin:
416
+ raise HTTPException(status_code=403, detail="Not enough permissions")
417
+
418
+ user = db.query(User).filter(User.id == user_id).first()
419
+ if user is None:
420
+ raise HTTPException(status_code=404, detail="User not found")
421
+
422
+ update_data = user_update.dict(exclude_unset=True)
423
+ for field, value in update_data.items():
424
+ setattr(user, field, value)
425
+
426
+ user.updated_at = datetime.utcnow()
427
+ db.commit()
428
+ db.refresh(user)
429
+
430
+ return user
431
+
432
+ # Product endpoints
433
+ @app.get("/products", response_model=List[ProductSchema], tags=["Products"])
434
+ async def get_products(
435
+ skip: int = 0,
436
+ limit: int = 100,
437
+ category: Optional[ProductCategory] = None,
438
+ search: Optional[str] = None,
439
+ db: Session = Depends(get_db)
440
+ ):
441
+ """Get list of products with optional filtering"""
442
+ query = db.query(Product).filter(Product.is_active == True)
443
+
444
+ if category:
445
+ query = query.filter(Product.category == category)
446
+
447
+ if search:
448
+ query = query.filter(Product.name.contains(search))
449
+
450
+ products = query.offset(skip).limit(limit).all()
451
+ return products
452
+
453
+ @app.get("/products/{product_id}", response_model=ProductSchema, tags=["Products"])
454
+ async def get_product(product_id: int, db: Session = Depends(get_db)):
455
+ """Get product by ID"""
456
+ product = db.query(Product).filter(Product.id == product_id, Product.is_active == True).first()
457
+ if product is None:
458
+ raise HTTPException(status_code=404, detail="Product not found")
459
+
460
+ return product
461
+
462
+ @app.post("/products", response_model=ProductSchema, tags=["Products"])
463
+ async def create_product(
464
+ product: ProductCreate,
465
+ current_user: User = Depends(get_current_user),
466
+ db: Session = Depends(get_db)
467
+ ):
468
+ """Create a new product (admin only)"""
469
+ if not current_user.is_admin:
470
+ raise HTTPException(status_code=403, detail="Not enough permissions")
471
+
472
+ db_product = Product(**product.dict())
473
+ db.add(db_product)
474
+ db.commit()
475
+ db.refresh(db_product)
476
+
477
+ return db_product
478
+
479
+ @app.put("/products/{product_id}", response_model=ProductSchema, tags=["Products"])
480
+ async def update_product(
481
+ product_id: int,
482
+ product_update: ProductUpdate,
483
+ current_user: User = Depends(get_current_user),
484
+ db: Session = Depends(get_db)
485
+ ):
486
+ """Update product (admin only)"""
487
+ if not current_user.is_admin:
488
+ raise HTTPException(status_code=403, detail="Not enough permissions")
489
+
490
+ product = db.query(Product).filter(Product.id == product_id).first()
491
+ if product is None:
492
+ raise HTTPException(status_code=404, detail="Product not found")
493
+
494
+ update_data = product_update.dict(exclude_unset=True)
495
+ for field, value in update_data.items():
496
+ setattr(product, field, value)
497
+
498
+ product.updated_at = datetime.utcnow()
499
+ db.commit()
500
+ db.refresh(product)
501
+
502
+ return product
503
+
504
+ # Order endpoints
505
+ @app.get("/orders", response_model=List[OrderSchema], tags=["Orders"])
506
+ async def get_orders(
507
+ skip: int = 0,
508
+ limit: int = 100,
509
+ status: Optional[OrderStatus] = None,
510
+ current_user: User = Depends(get_current_user),
511
+ db: Session = Depends(get_db)
512
+ ):
513
+ """Get user's orders"""
514
+ query = db.query(Order).filter(Order.user_id == current_user.id)
515
+
516
+ if status:
517
+ query = query.filter(Order.status == status)
518
+
519
+ orders = query.offset(skip).limit(limit).all()
520
+ return orders
521
+
522
+ @app.get("/orders/{order_id}", response_model=OrderSchema, tags=["Orders"])
523
+ async def get_order(
524
+ order_id: int,
525
+ current_user: User = Depends(get_current_user),
526
+ db: Session = Depends(get_db)
527
+ ):
528
+ """Get order by ID"""
529
+ order = db.query(Order).filter(Order.id == order_id).first()
530
+ if order is None:
531
+ raise HTTPException(status_code=404, detail="Order not found")
532
+
533
+ if order.user_id != current_user.id and not current_user.is_admin:
534
+ raise HTTPException(status_code=403, detail="Not enough permissions")
535
+
536
+ return order
537
+
538
+ @app.post("/orders", response_model=OrderSchema, tags=["Orders"])
539
+ async def create_order(
540
+ order: OrderCreate,
541
+ current_user: User = Depends(get_current_user),
542
+ db: Session = Depends(get_db)
543
+ ):
544
+ """Create a new order"""
545
+ total_amount = 0
546
+ order_items = []
547
+
548
+ # Calculate total and validate products
549
+ for item in order.items:
550
+ product = db.query(Product).filter(Product.id == item.product_id, Product.is_active == True).first()
551
+ if not product:
552
+ raise HTTPException(status_code=404, detail=f"Product {item.product_id} not found")
553
+
554
+ if product.stock_quantity < item.quantity:
555
+ raise HTTPException(status_code=400, detail=f"Insufficient stock for product {product.name}")
556
+
557
+ item_total = product.price * item.quantity
558
+ total_amount += item_total
559
+
560
+ order_items.append(OrderItem(
561
+ product_id=item.product_id,
562
+ quantity=item.quantity,
563
+ unit_price=product.price
564
+ ))
565
+
566
+ # Create order
567
+ db_order = Order(
568
+ user_id=current_user.id,
569
+ total_amount=total_amount,
570
+ shipping_address=order.shipping_address,
571
+ items=order_items
572
+ )
573
+
574
+ # Update product stock
575
+ for item in order.items:
576
+ product = db.query(Product).filter(Product.id == item.product_id).first()
577
+ product.stock_quantity -= item.quantity
578
+
579
+ db.add(db_order)
580
+ db.commit()
581
+ db.refresh(db_order)
582
+
583
+ return db_order
584
+
585
+ @app.put("/orders/{order_id}", response_model=OrderSchema, tags=["Orders"])
586
+ async def update_order(
587
+ order_id: int,
588
+ order_update: OrderUpdate,
589
+ current_user: User = Depends(get_current_user),
590
+ db: Session = Depends(get_db)
591
+ ):
592
+ """Update order status (admin only for status changes)"""
593
+ order = db.query(Order).filter(Order.id == order_id).first()
594
+ if order is None:
595
+ raise HTTPException(status_code=404, detail="Order not found")
596
+
597
+ # Check permissions
598
+ if order.user_id != current_user.id and not current_user.is_admin:
599
+ raise HTTPException(status_code=403, detail="Not enough permissions")
600
+
601
+ # Only admins can change status
602
+ if order_update.status and not current_user.is_admin:
603
+ raise HTTPException(status_code=403, detail="Only admins can change order status")
604
+
605
+ update_data = order_update.dict(exclude_unset=True)
606
+ for field, value in update_data.items():
607
+ setattr(order, field, value)
608
+
609
+ order.updated_at = datetime.utcnow()
610
+ db.commit()
611
+ db.refresh(order)
612
+
613
+ return order
614
+
615
+ if __name__ == "__main__":
616
+ uvicorn.run(app, host="0.0.0.0", port=8000)
617
+ ```
618
+
619
+ ### 1.5 Create Database Configuration
620
+
621
+ Create `backend/database.py`:
622
+
623
+ ```python
624
+ from sqlalchemy import create_engine
625
+ from sqlalchemy.orm import sessionmaker
626
+
627
+ DATABASE_URL = "sqlite:///./ecommerce.db"
628
+
629
+ engine = create_engine(
630
+ DATABASE_URL, connect_args={"check_same_thread": False}
631
+ )
632
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
633
+
634
+ def get_db():
635
+ db = SessionLocal()
636
+ try:
637
+ yield db
638
+ finally:
639
+ db.close()
640
+ ```
641
+
642
+ ### 1.6 Create Authentication System
643
+
644
+ Create `backend/auth.py`:
645
+
646
+ ```python
647
+ from datetime import datetime, timedelta
648
+ from typing import Optional
649
+ from fastapi import Depends, HTTPException, status
650
+ from fastapi.security import OAuth2PasswordBearer
651
+ from jose import JWTError, jwt
652
+ from passlib.context import CryptContext
653
+ from sqlalchemy.orm import Session
654
+
655
+ from models import User
656
+ from schemas import TokenData
657
+ from database import get_db
658
+
659
+ # Configuration
660
+ SECRET_KEY = "your-secret-key-here" # In production, use environment variable
661
+ ALGORITHM = "HS256"
662
+ ACCESS_TOKEN_EXPIRE_MINUTES = 30
663
+
664
+ # Password hashing
665
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
666
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
667
+
668
+ def verify_password(plain_password, hashed_password):
669
+ return pwd_context.verify(plain_password, hashed_password)
670
+
671
+ def get_password_hash(password):
672
+ return pwd_context.hash(password)
673
+
674
+ def authenticate_user(db: Session, username: str, password: str):
675
+ user = db.query(User).filter(
676
+ (User.username == username) | (User.email == username)
677
+ ).first()
678
+
679
+ if not user or not verify_password(password, user.hashed_password):
680
+ return False
681
+
682
+ return user
683
+
684
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
685
+ to_encode = data.copy()
686
+ if expires_delta:
687
+ expire = datetime.utcnow() + expires_delta
688
+ else:
689
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
690
+
691
+ to_encode.update({"exp": expire})
692
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
693
+ return encoded_jwt
694
+
695
+ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
696
+ credentials_exception = HTTPException(
697
+ status_code=status.HTTP_401_UNAUTHORIZED,
698
+ detail="Could not validate credentials",
699
+ headers={"WWW-Authenticate": "Bearer"},
700
+ )
701
+
702
+ try:
703
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
704
+ username: str = payload.get("sub")
705
+ if username is None:
706
+ raise credentials_exception
707
+ token_data = TokenData(username=username)
708
+ except JWTError:
709
+ raise credentials_exception
710
+
711
+ user = db.query(User).filter(User.username == token_data.username).first()
712
+ if user is None:
713
+ raise credentials_exception
714
+
715
+ return user
716
+ ```
717
+
718
+ ### 1.7 Run the FastAPI Server
719
+
720
+ ```bash
721
+ cd backend
722
+ uvicorn main:app --reload
723
+ ```
724
+
725
+ Your API will be available at http://localhost:8000, with interactive docs at http://localhost:8000/docs.
726
+
727
+ ## Part 2: Generate TypeScript Types with Schemantic
728
+
729
+ ### 2.1 Install Schemantic
730
+
731
+ ```bash
732
+ # Go back to project root
733
+ cd ..
734
+
735
+ # Install Schemantic
736
+ npm install -g schemantic
737
+ ```
738
+
739
+ ### 2.2 Generate Types and API Client
740
+
741
+ ```bash
742
+ # Create output directory
743
+ mkdir frontend/src/generated
744
+
745
+ # Generate types and API client with React hooks
746
+ npx schemantic generate \
747
+ --url http://localhost:8000/openapi.json \
748
+ --output ./frontend/src/generated \
749
+ --hooks \
750
+ --naming camelCase \
751
+ --prefix "API"
752
+
753
+ # Generated files:
754
+ # - types.ts (TypeScript interfaces)
755
+ # - api-client.ts (API client class)
756
+ # - hooks.ts (React hooks)
757
+ # - index.ts (barrel exports)
758
+ ```
759
+
760
+ ### 2.3 Verify Generated Types
761
+
762
+ Check the generated files:
763
+
764
+ ```typescript
765
+ // frontend/src/generated/types.ts
766
+ export interface APIUser {
767
+ id: number;
768
+ email: string;
769
+ username: string;
770
+ fullName?: string;
771
+ isActive: boolean;
772
+ isAdmin: boolean;
773
+ createdAt: string;
774
+ updatedAt: string;
775
+ }
776
+
777
+ export interface APIProduct {
778
+ id: number;
779
+ name: string;
780
+ description?: string;
781
+ price: number;
782
+ category: APIProductCategory;
783
+ stockQuantity: number;
784
+ imageUrl?: string;
785
+ isActive: boolean;
786
+ createdAt: string;
787
+ updatedAt: string;
788
+ }
789
+
790
+ export enum APIProductCategory {
791
+ ELECTRONICS = "electronics",
792
+ CLOTHING = "clothing",
793
+ BOOKS = "books",
794
+ HOME = "home",
795
+ SPORTS = "sports",
796
+ }
797
+
798
+ export enum APIOrderStatus {
799
+ PENDING = "pending",
800
+ PROCESSING = "processing",
801
+ SHIPPED = "shipped",
802
+ DELIVERED = "delivered",
803
+ CANCELLED = "cancelled",
804
+ }
805
+
806
+ // ... many more types
807
+ ```
808
+
809
+ ## Part 3: React Frontend Setup
810
+
811
+ ### 3.1 Create React Application
812
+
813
+ ```bash
814
+ # Create React app with TypeScript
815
+ npx create-react-app frontend --template typescript
816
+ cd frontend
817
+
818
+ # Install additional dependencies
819
+ npm install @tanstack/react-query axios react-router-dom @types/react-router-dom
820
+ npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
821
+ ```
822
+
823
+ ### 3.2 Configure API Client
824
+
825
+ Create `frontend/src/api/client.ts`:
826
+
827
+ ```typescript
828
+ import { ECommerceApiClient } from "../generated";
829
+
830
+ // Create API client instance
831
+ export const apiClient = new ECommerceApiClient({
832
+ baseUrl: "http://localhost:8000",
833
+ headers: {
834
+ "Content-Type": "application/json",
835
+ },
836
+ });
837
+
838
+ // Add authentication token to requests
839
+ export const setAuthToken = (token: string) => {
840
+ apiClient.setDefaultHeader("Authorization", `Bearer ${token}`);
841
+ };
842
+
843
+ export const clearAuthToken = () => {
844
+ apiClient.removeDefaultHeader("Authorization");
845
+ };
846
+ ```
847
+
848
+ ### 3.3 Setup React Query and Hooks
849
+
850
+ Create `frontend/src/hooks/api.ts`:
851
+
852
+ ```typescript
853
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
854
+ import { apiClient } from "../api/client";
855
+ import { createApiHooks } from "../generated";
856
+
857
+ // Create typed hooks
858
+ export const apiHooks = createApiHooks(apiClient);
859
+
860
+ // Export specific hooks for easy use
861
+ export const {
862
+ // Authentication hooks
863
+ useAuthRegisterMutation,
864
+ useAuthLoginMutation,
865
+ useAuthMeQuery,
866
+
867
+ // User hooks
868
+ useUsersQuery,
869
+ useUsersUserIdQuery,
870
+ useUsersUserIdMutation,
871
+
872
+ // Product hooks
873
+ useProductsQuery,
874
+ useProductsProductIdQuery,
875
+ useProductsMutation,
876
+ useProductsProductIdMutation,
877
+
878
+ // Order hooks
879
+ useOrdersQuery,
880
+ useOrdersOrderIdQuery,
881
+ useOrdersMutation,
882
+ useOrdersOrderIdMutation,
883
+ } = apiHooks;
884
+
885
+ // Custom hooks for common patterns
886
+ export const useAuth = () => {
887
+ const queryClient = useQueryClient();
888
+
889
+ const loginMutation = useAuthLoginMutation({
890
+ onSuccess: (data) => {
891
+ localStorage.setItem("token", data.accessToken);
892
+ queryClient.invalidateQueries(["auth", "me"]);
893
+ },
894
+ });
895
+
896
+ const logout = () => {
897
+ localStorage.removeItem("token");
898
+ queryClient.clear();
899
+ };
900
+
901
+ return {
902
+ login: loginMutation.mutate,
903
+ logout,
904
+ isLoading: loginMutation.isLoading,
905
+ error: loginMutation.error,
906
+ };
907
+ };
908
+
909
+ export const useProducts = (filters?: {
910
+ category?: string;
911
+ search?: string;
912
+ page?: number;
913
+ limit?: number;
914
+ }) => {
915
+ return useProductsQuery({
916
+ query: {
917
+ skip: ((filters?.page || 1) - 1) * (filters?.limit || 20),
918
+ limit: filters?.limit || 20,
919
+ category: filters?.category,
920
+ search: filters?.search,
921
+ },
922
+ });
923
+ };
924
+
925
+ export const useUserOrders = (userId?: number) => {
926
+ return useOrdersQuery({
927
+ enabled: !!userId,
928
+ });
929
+ };
930
+ ```
931
+
932
+ ### 3.4 Create React Components
933
+
934
+ Create `frontend/src/components/ProductList.tsx`:
935
+
936
+ ```typescript
937
+ import React, { useState } from "react";
938
+ import {
939
+ Grid,
940
+ Card,
941
+ CardContent,
942
+ CardMedia,
943
+ Typography,
944
+ Button,
945
+ TextField,
946
+ Select,
947
+ MenuItem,
948
+ FormControl,
949
+ InputLabel,
950
+ Box,
951
+ CircularProgress,
952
+ Alert,
953
+ } from "@mui/material";
954
+ import { APIProduct, APIProductCategory } from "../generated";
955
+ import { useProducts } from "../hooks/api";
956
+
957
+ interface ProductListProps {
958
+ onAddToCart?: (product: APIProduct) => void;
959
+ }
960
+
961
+ export const ProductList: React.FC<ProductListProps> = ({ onAddToCart }) => {
962
+ const [search, setSearch] = useState("");
963
+ const [category, setCategory] = useState<APIProductCategory | "">("");
964
+ const [page, setPage] = useState(1);
965
+
966
+ const {
967
+ data: products,
968
+ isLoading,
969
+ error,
970
+ } = useProducts({
971
+ search: search || undefined,
972
+ category: category || undefined,
973
+ page,
974
+ limit: 12,
975
+ });
976
+
977
+ if (isLoading) {
978
+ return (
979
+ <Box display="flex" justifyContent="center" p={4}>
980
+ <CircularProgress />
981
+ </Box>
982
+ );
983
+ }
984
+
985
+ if (error) {
986
+ return (
987
+ <Alert severity="error">Failed to load products: {error.message}</Alert>
988
+ );
989
+ }
990
+
991
+ return (
992
+ <Box>
993
+ {/* Filters */}
994
+ <Box display="flex" gap={2} mb={3}>
995
+ <TextField
996
+ label="Search products"
997
+ value={search}
998
+ onChange={(e) => setSearch(e.target.value)}
999
+ variant="outlined"
1000
+ size="small"
1001
+ sx={{ minWidth: 200 }}
1002
+ />
1003
+
1004
+ <FormControl size="small" sx={{ minWidth: 150 }}>
1005
+ <InputLabel>Category</InputLabel>
1006
+ <Select
1007
+ value={category}
1008
+ onChange={(e) => setCategory(e.target.value as APIProductCategory)}
1009
+ label="Category"
1010
+ >
1011
+ <MenuItem value="">All Categories</MenuItem>
1012
+ {Object.values(APIProductCategory).map((cat) => (
1013
+ <MenuItem key={cat} value={cat}>
1014
+ {cat.charAt(0).toUpperCase() + cat.slice(1)}
1015
+ </MenuItem>
1016
+ ))}
1017
+ </Select>
1018
+ </FormControl>
1019
+ </Box>
1020
+
1021
+ {/* Product Grid */}
1022
+ <Grid container spacing={3}>
1023
+ {products?.map((product) => (
1024
+ <Grid item xs={12} sm={6} md={4} lg={3} key={product.id}>
1025
+ <Card>
1026
+ {product.imageUrl && (
1027
+ <CardMedia
1028
+ component="img"
1029
+ height="200"
1030
+ image={product.imageUrl}
1031
+ alt={product.name}
1032
+ />
1033
+ )}
1034
+
1035
+ <CardContent>
1036
+ <Typography variant="h6" component="h2" gutterBottom>
1037
+ {product.name}
1038
+ </Typography>
1039
+
1040
+ <Typography variant="body2" color="text.secondary" paragraph>
1041
+ {product.description}
1042
+ </Typography>
1043
+
1044
+ <Typography variant="h6" color="primary" gutterBottom>
1045
+ ${product.price.toFixed(2)}
1046
+ </Typography>
1047
+
1048
+ <Typography variant="body2" color="text.secondary">
1049
+ Stock: {product.stockQuantity}
1050
+ </Typography>
1051
+
1052
+ <Button
1053
+ variant="contained"
1054
+ fullWidth
1055
+ sx={{ mt: 2 }}
1056
+ disabled={product.stockQuantity === 0}
1057
+ onClick={() => onAddToCart?.(product)}
1058
+ >
1059
+ {product.stockQuantity === 0 ? "Out of Stock" : "Add to Cart"}
1060
+ </Button>
1061
+ </CardContent>
1062
+ </Card>
1063
+ </Grid>
1064
+ ))}
1065
+ </Grid>
1066
+ </Box>
1067
+ );
1068
+ };
1069
+ ```
1070
+
1071
+ Create `frontend/src/components/UserProfile.tsx`:
1072
+
1073
+ ```typescript
1074
+ import React from "react";
1075
+ import {
1076
+ Card,
1077
+ CardContent,
1078
+ Typography,
1079
+ Button,
1080
+ Box,
1081
+ Chip,
1082
+ CircularProgress,
1083
+ Alert,
1084
+ } from "@mui/material";
1085
+ import { useAuthMeQuery } from "../hooks/api";
1086
+
1087
+ export const UserProfile: React.FC = () => {
1088
+ const { data: user, isLoading, error } = useAuthMeQuery();
1089
+
1090
+ if (isLoading) {
1091
+ return (
1092
+ <Box display="flex" justifyContent="center" p={4}>
1093
+ <CircularProgress />
1094
+ </Box>
1095
+ );
1096
+ }
1097
+
1098
+ if (error) {
1099
+ return (
1100
+ <Alert severity="error">
1101
+ Failed to load user profile: {error.message}
1102
+ </Alert>
1103
+ );
1104
+ }
1105
+
1106
+ if (!user) {
1107
+ return <Alert severity="info">Please log in to view your profile.</Alert>;
1108
+ }
1109
+
1110
+ return (
1111
+ <Card>
1112
+ <CardContent>
1113
+ <Typography variant="h5" component="h1" gutterBottom>
1114
+ User Profile
1115
+ </Typography>
1116
+
1117
+ <Box display="flex" flexDirection="column" gap={2}>
1118
+ <Box>
1119
+ <Typography variant="subtitle1" fontWeight="bold">
1120
+ Username:
1121
+ </Typography>
1122
+ <Typography>{user.username}</Typography>
1123
+ </Box>
1124
+
1125
+ <Box>
1126
+ <Typography variant="subtitle1" fontWeight="bold">
1127
+ Email:
1128
+ </Typography>
1129
+ <Typography>{user.email}</Typography>
1130
+ </Box>
1131
+
1132
+ {user.fullName && (
1133
+ <Box>
1134
+ <Typography variant="subtitle1" fontWeight="bold">
1135
+ Full Name:
1136
+ </Typography>
1137
+ <Typography>{user.fullName}</Typography>
1138
+ </Box>
1139
+ )}
1140
+
1141
+ <Box>
1142
+ <Typography variant="subtitle1" fontWeight="bold">
1143
+ Status:
1144
+ </Typography>
1145
+ <Box display="flex" gap={1}>
1146
+ <Chip
1147
+ label={user.isActive ? "Active" : "Inactive"}
1148
+ color={user.isActive ? "success" : "error"}
1149
+ size="small"
1150
+ />
1151
+ {user.isAdmin && (
1152
+ <Chip label="Admin" color="primary" size="small" />
1153
+ )}
1154
+ </Box>
1155
+ </Box>
1156
+
1157
+ <Box>
1158
+ <Typography variant="subtitle1" fontWeight="bold">
1159
+ Member Since:
1160
+ </Typography>
1161
+ <Typography>
1162
+ {new Date(user.createdAt).toLocaleDateString()}
1163
+ </Typography>
1164
+ </Box>
1165
+ </Box>
1166
+
1167
+ <Button
1168
+ variant="outlined"
1169
+ sx={{ mt: 3 }}
1170
+ onClick={() => {
1171
+ // Handle edit profile
1172
+ }}
1173
+ >
1174
+ Edit Profile
1175
+ </Button>
1176
+ </CardContent>
1177
+ </Card>
1178
+ );
1179
+ };
1180
+ ```
1181
+
1182
+ Create `frontend/src/components/OrderHistory.tsx`:
1183
+
1184
+ ```typescript
1185
+ import React from "react";
1186
+ import {
1187
+ Table,
1188
+ TableBody,
1189
+ TableCell,
1190
+ TableContainer,
1191
+ TableHead,
1192
+ TableRow,
1193
+ Paper,
1194
+ Chip,
1195
+ Typography,
1196
+ Box,
1197
+ CircularProgress,
1198
+ Alert,
1199
+ } from "@mui/material";
1200
+ import { APIOrderStatus } from "../generated";
1201
+ import { useOrdersQuery } from "../hooks/api";
1202
+
1203
+ const getStatusColor = (status: APIOrderStatus) => {
1204
+ switch (status) {
1205
+ case APIOrderStatus.PENDING:
1206
+ return "warning";
1207
+ case APIOrderStatus.PROCESSING:
1208
+ return "info";
1209
+ case APIOrderStatus.SHIPPED:
1210
+ return "primary";
1211
+ case APIOrderStatus.DELIVERED:
1212
+ return "success";
1213
+ case APIOrderStatus.CANCELLED:
1214
+ return "error";
1215
+ default:
1216
+ return "default";
1217
+ }
1218
+ };
1219
+
1220
+ export const OrderHistory: React.FC = () => {
1221
+ const { data: orders, isLoading, error } = useOrdersQuery();
1222
+
1223
+ if (isLoading) {
1224
+ return (
1225
+ <Box display="flex" justifyContent="center" p={4}>
1226
+ <CircularProgress />
1227
+ </Box>
1228
+ );
1229
+ }
1230
+
1231
+ if (error) {
1232
+ return (
1233
+ <Alert severity="error">Failed to load orders: {error.message}</Alert>
1234
+ );
1235
+ }
1236
+
1237
+ if (!orders || orders.length === 0) {
1238
+ return <Alert severity="info">You haven't placed any orders yet.</Alert>;
1239
+ }
1240
+
1241
+ return (
1242
+ <Box>
1243
+ <Typography variant="h5" component="h1" gutterBottom>
1244
+ Order History
1245
+ </Typography>
1246
+
1247
+ <TableContainer component={Paper}>
1248
+ <Table>
1249
+ <TableHead>
1250
+ <TableRow>
1251
+ <TableCell>Order ID</TableCell>
1252
+ <TableCell>Date</TableCell>
1253
+ <TableCell>Items</TableCell>
1254
+ <TableCell>Total</TableCell>
1255
+ <TableCell>Status</TableCell>
1256
+ </TableRow>
1257
+ </TableHead>
1258
+ <TableBody>
1259
+ {orders.map((order) => (
1260
+ <TableRow key={order.id}>
1261
+ <TableCell>#{order.id}</TableCell>
1262
+ <TableCell>
1263
+ {new Date(order.createdAt).toLocaleDateString()}
1264
+ </TableCell>
1265
+ <TableCell>
1266
+ {order.items.length} item{order.items.length !== 1 ? "s" : ""}
1267
+ </TableCell>
1268
+ <TableCell>${order.totalAmount.toFixed(2)}</TableCell>
1269
+ <TableCell>
1270
+ <Chip
1271
+ label={order.status.toUpperCase()}
1272
+ color={getStatusColor(order.status)}
1273
+ size="small"
1274
+ />
1275
+ </TableCell>
1276
+ </TableRow>
1277
+ ))}
1278
+ </TableBody>
1279
+ </Table>
1280
+ </TableContainer>
1281
+ </Box>
1282
+ );
1283
+ };
1284
+ ```
1285
+
1286
+ ### 3.5 Setup App with React Query
1287
+
1288
+ Update `frontend/src/App.tsx`:
1289
+
1290
+ ```typescript
1291
+ import React from "react";
1292
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
1293
+ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
1294
+ import { ThemeProvider, createTheme, CssBaseline } from "@mui/material";
1295
+ import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
1296
+ import { ProductList } from "./components/ProductList";
1297
+ import { UserProfile } from "./components/UserProfile";
1298
+ import { OrderHistory } from "./components/OrderHistory";
1299
+
1300
+ // Create React Query client
1301
+ const queryClient = new QueryClient({
1302
+ defaultOptions: {
1303
+ queries: {
1304
+ retry: 1,
1305
+ refetchOnWindowFocus: false,
1306
+ },
1307
+ },
1308
+ });
1309
+
1310
+ // Create Material-UI theme
1311
+ const theme = createTheme({
1312
+ palette: {
1313
+ mode: "light",
1314
+ primary: {
1315
+ main: "#1976d2",
1316
+ },
1317
+ secondary: {
1318
+ main: "#dc004e",
1319
+ },
1320
+ },
1321
+ });
1322
+
1323
+ function App() {
1324
+ return (
1325
+ <QueryClientProvider client={queryClient}>
1326
+ <ThemeProvider theme={theme}>
1327
+ <CssBaseline />
1328
+ <Router>
1329
+ <div className="App">
1330
+ <Routes>
1331
+ <Route path="/" element={<ProductList />} />
1332
+ <Route path="/profile" element={<UserProfile />} />
1333
+ <Route path="/orders" element={<OrderHistory />} />
1334
+ </Routes>
1335
+ </div>
1336
+ </Router>
1337
+ <ReactQueryDevtools initialIsOpen={false} />
1338
+ </ThemeProvider>
1339
+ </QueryClientProvider>
1340
+ );
1341
+ }
1342
+
1343
+ export default App;
1344
+ ```
1345
+
1346
+ ## Part 4: Run the Complete Application
1347
+
1348
+ ### 4.1 Start the Backend
1349
+
1350
+ ```bash
1351
+ cd backend
1352
+ source venv/bin/activate # On Windows: venv\Scripts\activate
1353
+ uvicorn main:app --reload
1354
+ ```
1355
+
1356
+ ### 4.2 Start the Frontend
1357
+
1358
+ ```bash
1359
+ cd frontend
1360
+ npm start
1361
+ ```
1362
+
1363
+ ### 4.3 Test the Integration
1364
+
1365
+ 1. **Visit the API docs**: http://localhost:8000/docs
1366
+ 2. **Visit the React app**: http://localhost:3000
1367
+ 3. **Test the type safety**: Try modifying API responses and see TypeScript errors in the frontend
1368
+
1369
+ ## Part 5: Advanced Features
1370
+
1371
+ ### 5.1 Regenerate Types on API Changes
1372
+
1373
+ Create `scripts/update-types.sh`:
1374
+
1375
+ ```bash
1376
+ #!/bin/bash
1377
+ echo "🔄 Updating TypeScript types from API..."
1378
+
1379
+ # Check if API is running
1380
+ if ! curl -s http://localhost:8000/health > /dev/null; then
1381
+ echo "❌ API server is not running. Please start it first."
1382
+ exit 1
1383
+ fi
1384
+
1385
+ # Generate new types
1386
+ npx schemantic generate \
1387
+ --url http://localhost:8000/openapi.json \
1388
+ --output ./frontend/src/generated \
1389
+ --hooks \
1390
+ --naming camelCase \
1391
+ --prefix "API"
1392
+
1393
+ echo "✅ Types updated successfully!"
1394
+ echo "🏗️ Building frontend to verify types..."
1395
+
1396
+ cd frontend
1397
+ npm run build
1398
+
1399
+ if [ $? -eq 0 ]; then
1400
+ echo "✅ Frontend builds successfully with new types!"
1401
+ else
1402
+ echo "❌ Frontend build failed. Please check for type errors."
1403
+ exit 1
1404
+ fi
1405
+ ```
1406
+
1407
+ ### 5.2 Add Type-Safe Error Handling
1408
+
1409
+ Create `frontend/src/utils/errors.ts`:
1410
+
1411
+ ```typescript
1412
+ import { APIResponse } from "../generated";
1413
+
1414
+ export class APIError extends Error {
1415
+ constructor(message: string, public status: number, public response?: any) {
1416
+ super(message);
1417
+ this.name = "APIError";
1418
+ }
1419
+ }
1420
+
1421
+ export const handleAPIError = (error: any): string => {
1422
+ if (error instanceof APIError) {
1423
+ return `API Error (${error.status}): ${error.message}`;
1424
+ }
1425
+
1426
+ if (error.response?.data?.message) {
1427
+ return error.response.data.message;
1428
+ }
1429
+
1430
+ return error.message || "An unexpected error occurred";
1431
+ };
1432
+ ```
1433
+
1434
+ ### 5.3 Add Real-time Updates with WebSockets
1435
+
1436
+ The generated types can also be used with WebSocket events:
1437
+
1438
+ ```typescript
1439
+ // frontend/src/hooks/useWebSocket.ts
1440
+ import { useEffect, useState } from "react";
1441
+ import { APIOrder } from "../generated";
1442
+
1443
+ export const useOrderUpdates = (userId: number) => {
1444
+ const [orders, setOrders] = useState<APIOrder[]>([]);
1445
+
1446
+ useEffect(() => {
1447
+ const ws = new WebSocket(`ws://localhost:8000/ws/orders/${userId}`);
1448
+
1449
+ ws.onmessage = (event) => {
1450
+ const data = JSON.parse(event.data);
1451
+
1452
+ // Type-safe handling of WebSocket messages
1453
+ if (data.type === "order_update") {
1454
+ const updatedOrder: APIOrder = data.order;
1455
+ setOrders((prev) =>
1456
+ prev.map((order) =>
1457
+ order.id === updatedOrder.id ? updatedOrder : order
1458
+ )
1459
+ );
1460
+ }
1461
+ };
1462
+
1463
+ return () => ws.close();
1464
+ }, [userId]);
1465
+
1466
+ return orders;
1467
+ };
1468
+ ```
1469
+
1470
+ ## Conclusion
1471
+
1472
+ You now have a complete, type-safe full-stack application with:
1473
+
1474
+ 1. ✅ **FastAPI backend** with comprehensive API
1475
+ 2. ✅ **Generated TypeScript types** that match your API exactly
1476
+ 3. ✅ **Type-safe API client** with error handling
1477
+ 4. ✅ **React hooks** for data fetching with React Query
1478
+ 5. ✅ **Full type safety** from database to UI
1479
+ 6. ✅ **Automatic synchronization** between backend and frontend types
1480
+
1481
+ ### Key Benefits
1482
+
1483
+ - **Type Safety**: Catch API integration errors at compile time
1484
+ - **Developer Experience**: Auto-completion and IntelliSense for API calls
1485
+ - **Maintainability**: Types automatically update when API changes
1486
+ - **Performance**: Optimized data fetching with React Query
1487
+ - **Scalability**: Clean separation of concerns and modular architecture
1488
+
1489
+ ### Next Steps
1490
+
1491
+ 1. **Add authentication persistence** with localStorage
1492
+ 2. **Implement real-time features** with WebSockets
1493
+ 3. **Add comprehensive error handling** throughout the app
1494
+ 4. **Set up automated type generation** in your CI/CD pipeline
1495
+ 5. **Add testing** with the generated types for both frontend and backend
1496
+
1497
+ This tutorial demonstrates the power of Schemantic in creating maintainable, type-safe full-stack applications where the frontend and backend always stay in sync!