ЧАСТЬ IV. ПРАКТИЧЕСКОЕ ПРИМЕНЕНИЕ
Глава 13. Web-разработка в Cursor
13.1. Frontend: React, Vue, Angular
React Development с Cursor
Cursor особенно эффективен для React-разработки благодаря глубокому пониманию паттернов и современных практик.
Создание React компонента с нуля
You: Создай компонент ProductCard для e-commerce приложения
AI:// components/ProductCard/ProductCard.tsx
import React, { useState, useCallback } from 'react';
import { Star, ShoppingCart, Heart } from 'lucide-react';
import styles from './ProductCard.module.css';
export interface Product {
id: string;
name: string;
description: string;
price: number;
originalPrice?: number;
imageUrl: string;
rating: number;
reviewCount: number;
inStock: boolean;
badge?: 'new' | 'sale' | 'bestseller';
}
export interface ProductCardProps {
product: Product;
onAddToCart?: (productId: string) => void;
onToggleWishlist?: (productId: string) => void;
isInWishlist?: boolean;
variant?: 'default' | 'compact' | 'detailed';
}
export const ProductCard: React.FC<ProductCardProps> = ({
product,
onAddToCart,
onToggleWishlist,
isInWishlist = false,
variant = 'default'
}) => {
const [imageLoaded, setImageLoaded] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const handleAddToCart = useCallback((e: React.MouseEvent) => {
e.preventDefault();
if (product.inStock && onAddToCart) {
onAddToCart(product.id);
}
}, [product.id, product.inStock, onAddToCart]);
const handleToggleWishlist = useCallback((e: React.MouseEvent) => {
e.preventDefault();
if (onToggleWishlist) {
onToggleWishlist(product.id);
}
}, [product.id, onToggleWishlist]);
const discountPercentage = product.originalPrice
? Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)
: 0;
const getBadgeText = () => {
switch (product.badge) {
case 'new': return 'New';
case 'sale': return `${discountPercentage}% OFF`;
case 'bestseller': return 'Bestseller';
default: return null;
}
};
return (
<div
className={`${styles.card} ${styles[variant]}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
role="article"
aria-label={`Product: ${product.name}`}
>
{/* Badge */}
{product.badge && (
<div className={`${styles.badge} ${styles[`badge-${product.badge}`]}`}>
{getBadgeText()}
</div>
)}
{/* Wishlist Button */}
<button
className={`${styles.wishlistBtn} ${isInWishlist ? styles.active : ''}`}
onClick={handleToggleWishlist}
aria-label={isInWishlist ? 'Remove from wishlist' : 'Add to wishlist'}
>
<Heart
size={20}
fill={isInWishlist ? 'currentColor' : 'none'}
/>
</button>
{/* Image */}
<div className={styles.imageWrapper}>
{!imageLoaded && (
<div className={styles.imageSkeleton} />
)}
<img
src={product.imageUrl}
alt={product.name}
className={`${styles.image} ${imageLoaded ? styles.loaded : ''}`}
onLoad={() => setImageLoaded(true)}
loading="lazy"
/>
</div>
{/* Content */}
<div className={styles.content}>
<h3 className={styles.name}>{product.name}</h3>
{variant === 'detailed' && (
<p className={styles.description}>{product.description}</p>
)}
{/* Rating */}
<div className={styles.rating}>
<div className={styles.stars}>
{[...Array(5)].map((_, i) => (
<Star
key={i}
size={16}
fill={i < Math.floor(product.rating) ? 'currentColor' : 'none'}
className={styles.star}
/>
))}
</div>
<span className={styles.reviewCount}>
({product.reviewCount})
</span>
</div>
{/* Price */}
<div className={styles.priceWrapper}>
<span className={styles.price}>
${product.price.toFixed(2)}
</span>
{product.originalPrice && (
<span className={styles.originalPrice}>
${product.originalPrice.toFixed(2)}
</span>
)}
</div>
{/* Stock Status */}
{!product.inStock && (
<div className={styles.outOfStock}>Out of Stock</div>
)}
{/* Add to Cart Button */}
<button
className={`${styles.addToCartBtn} ${!product.inStock ? styles.disabled : ''}`}
onClick={handleAddToCart}
disabled={!product.inStock}
aria-label="Add to cart"
>
<ShoppingCart size={18} />
<span>Add to Cart</span>
</button>
</div>
{/* Quick View Overlay (on hover) */}
{isHovered && variant === 'default' && (
<div className={styles.quickView}>
<button className={styles.quickViewBtn}>
Quick View
</button>
</div>
)}
</div>
);
};
export default ProductCard;
/* components/ProductCard/ProductCard.module.css */
.card {
position: relative;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.badge {
position: absolute;
top: 12px;
left: 12px;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
z-index: 2;
}
.badge-new {
background: #4CAF50;
color: white;
}
.badge-sale {
background: #f44336;
color: white;
}
.badge-bestseller {
background: #FF9800;
color: white;
}
.wishlistBtn {
position: absolute;
top: 12px;
right: 12px;
width: 36px;
height: 36px;
border: none;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
z-index: 2;
color: #666;
}
.wishlistBtn:hover {
background: #f5f5f5;
color: #e91e63;
}
.wishlistBtn.active {
color: #e91e63;
}
.imageWrapper {
position: relative;
width: 100%;
padding-top: 100%; /* 1:1 Aspect Ratio */
overflow: hidden;
background: #f5f5f5;
}
.imageSkeleton {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.3s ease;
}
.image.loaded {
opacity: 1;
}
.content {
padding: 16px;
}
.name {
font-size: 16px;
font-weight: 600;
margin: 0 0 8px;
color: #333;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.description {
font-size: 14px;
color: #666;
margin: 0 0 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.rating {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.stars {
display: flex;
gap: 2px;
color: #FFB800;
}
.reviewCount {
font-size: 14px;
color: #666;
}
.priceWrapper {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.price {
font-size: 24px;
font-weight: 700;
color: #333;
}
.originalPrice {
font-size: 16px;
color: #999;
text-decoration: line-through;
}
.outOfStock {
padding: 8px;
background: #ffebee;
color: #c62828;
border-radius: 4px;
text-align: center;
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
}
.addToCartBtn {
width: 100%;
padding: 12px;
border: none;
background: #2196F3;
color: white;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background 0.2s ease;
}
.addToCartBtn:hover:not(.disabled) {
background: #1976D2;
}
.addToCartBtn.disabled {
background: #ccc;
cursor: not-allowed;
}
.quickView {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
display: flex;
justify-content: center;
animation: slideUp 0.2s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.quickViewBtn {
padding: 8px 16px;
background: white;
border: none;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
}
/* Variants */
.compact .content {
padding: 12px;
}
.compact .name {
font-size: 14px;
}
.compact .price {
font-size: 18px;
}
.detailed .imageWrapper {
padding-top: 75%; /* 4:3 Aspect Ratio */
}
Custom Hooks
You: Создай custom hook для pagination
AI:// hooks/usePagination.ts
import { useState, useCallback, useMemo } from 'react';
export interface UsePaginationProps {
totalItems: number;
itemsPerPage?: number;
initialPage?: number;
maxPageButtons?: number;
}
export interface UsePaginationReturn {
currentPage: number;
totalPages: number;
pageItems: number[];
isFirstPage: boolean;
isLastPage: boolean;
goToPage: (page: number) => void;
nextPage: () => void;
previousPage: () => void;
goToFirstPage: () => void;
goToLastPage: () => void;
startIndex: number;
endIndex: number;
visiblePages: number[];
}
export function usePagination({
totalItems,
itemsPerPage = 10,
initialPage = 1,
maxPageButtons = 7
}: UsePaginationProps): UsePaginationReturn {
const [currentPage, setCurrentPage] = useState(initialPage);
const totalPages = useMemo(
() => Math.ceil(totalItems / itemsPerPage),
[totalItems, itemsPerPage]
);
const isFirstPage = currentPage === 1;
const isLastPage = currentPage === totalPages;
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, totalItems);
const goToPage = useCallback((page: number) => {
const pageNumber = Math.max(1, Math.min(page, totalPages));
setCurrentPage(pageNumber);
}, [totalPages]);
const nextPage = useCallback(() => {
goToPage(currentPage + 1);
}, [currentPage, goToPage]);
const previousPage = useCallback(() => {
goToPage(currentPage - 1);
}, [currentPage, goToPage]);
const goToFirstPage = useCallback(() => {
goToPage(1);
}, [goToPage]);
const goToLastPage = useCallback(() => {
goToPage(totalPages);
}, [totalPages, goToPage]);
// Calculate visible page numbers
const visiblePages = useMemo(() => {
if (totalPages <= maxPageButtons) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
const halfButtons = Math.floor(maxPageButtons / 2);
let startPage = Math.max(currentPage - halfButtons, 1);
let endPage = Math.min(startPage + maxPageButtons - 1, totalPages);
if (endPage - startPage + 1 < maxPageButtons) {
startPage = Math.max(endPage - maxPageButtons + 1, 1);
}
const pages: number[] = [];
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
}, [currentPage, totalPages, maxPageButtons]);
const pageItems = useMemo(
() => Array.from({ length: endIndex - startIndex }, (_, i) => startIndex + i),
[startIndex, endIndex]
);
return {
currentPage,
totalPages,
pageItems,
isFirstPage,
isLastPage,
goToPage,
nextPage,
previousPage,
goToFirstPage,
goToLastPage,
startIndex,
endIndex,
visiblePages
};
}
// Usage example:
// const pagination = usePagination({
// totalItems: 100,
// itemsPerPage: 10
// });
Vue.js Component
You: Конвертируй этот React компонент в Vue 3 с Composition API
AI:<!-- components/ProductCard.vue -->
<template>
<div
:class="['product-card', `variant-${variant}`]"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
role="article"
:aria-label="`Product: ${product.name}`"
>
<!-- Badge -->
<div
v-if="product.badge"
:class="['badge', `badge-${product.badge}`]"
>
{{ badgeText }}
</div>
<!-- Wishlist Button -->
<button
:class="['wishlist-btn', { active: isInWishlist }]"
@click.prevent="handleToggleWishlist"
:aria-label="isInWishlist ? 'Remove from wishlist' : 'Add to wishlist'"
>
<Heart
:size="20"
:fill="isInWishlist ? 'currentColor' : 'none'"
/>
</button>
<!-- Image -->
<div class="image-wrapper">
<div v-if="!imageLoaded" class="image-skeleton" />
<img
:src="product.imageUrl"
:alt="product.name"
:class="['image', { loaded: imageLoaded }]"
@load="imageLoaded = true"
loading="lazy"
/>
</div>
<!-- Content -->
<div class="content">
<h3 class="name">{{ product.name }}</h3>
<p v-if="variant === 'detailed'" class="description">
{{ product.description }}
</p>
<!-- Rating -->
<div class="rating">
<div class="stars">
<Star
v-for="i in 5"
:key="i"
:size="16"
:fill="i <= Math.floor(product.rating) ? 'currentColor' : 'none'"
class="star"
/>
</div>
<span class="review-count">({{ product.reviewCount }})</span>
</div>
<!-- Price -->
<div class="price-wrapper">
<span class="price">${{ product.price.toFixed(2) }}</span>
<span v-if="product.originalPrice" class="original-price">
${{ product.originalPrice.toFixed(2) }}
</span>
</div>
<!-- Stock Status -->
<div v-if="!product.inStock" class="out-of-stock">
Out of Stock
</div>
<!-- Add to Cart Button -->
<button
:class="['add-to-cart-btn', { disabled: !product.inStock }]"
@click.prevent="handleAddToCart"
:disabled="!product.inStock"
aria-label="Add to cart"
>
<ShoppingCart :size="18" />
<span>Add to Cart</span>
</button>
</div>
<!-- Quick View Overlay -->
<div v-if="isHovered && variant === 'default'" class="quick-view">
<button class="quick-view-btn">Quick View</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { Star, ShoppingCart, Heart } from 'lucide-vue-next';
export interface Product {
id: string;
name: string;
description: string;
price: number;
originalPrice?: number;
imageUrl: string;
rating: number;
reviewCount: number;
inStock: boolean;
badge?: 'new' | 'sale' | 'bestseller';
}
interface Props {
product: Product;
isInWishlist?: boolean;
variant?: 'default' | 'compact' | 'detailed';
}
const props = withDefaults(defineProps<Props>(), {
isInWishlist: false,
variant: 'default'
});
const emit = defineEmits<{
addToCart: [productId: string];
toggleWishlist: [productId: string];
}>();
const imageLoaded = ref(false);
const isHovered = ref(false);
const discountPercentage = computed(() => {
if (!props.product.originalPrice) return 0;
return Math.round(
((props.product.originalPrice - props.product.price) / props.product.originalPrice) * 100
);
});
const badgeText = computed(() => {
switch (props.product.badge) {
case 'new': return 'New';
case 'sale': return `${discountPercentage.value}% OFF`;
case 'bestseller': return 'Bestseller';
default: return '';
}
});
const handleAddToCart = () => {
if (props.product.inStock) {
emit('addToCart', props.product.id);
}
};
const handleToggleWishlist = () => {
emit('toggleWishlist', props.product.id);
};
</script>
<style scoped>
/* Same CSS as React version */
.product-card {
position: relative;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
}
.product-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
/* ... rest of the styles ... */
</style>
Angular Component
You: Теперь создай версию для Angular
AI:// product-card.component.ts
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
export interface Product {
id: string;
name: string;
description: string;
price: number;
originalPrice?: number;
imageUrl: string;
rating: number;
reviewCount: number;
inStock: boolean;
badge?: 'new' | 'sale' | 'bestseller';
}
@Component({
selector: 'app-product-card',
standalone: true,
imports: [CommonModule],
templateUrl: './product-card.component.html',
styleUrls: ['./product-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductCardComponent {
@Input() product!: Product;
@Input() isInWishlist = false;
@Input() variant: 'default' | 'compact' | 'detailed' = 'default';
@Output() addToCart = new EventEmitter<string>();
@Output() toggleWishlist = new EventEmitter<string>();
imageLoaded = false;
isHovered = false;
get discountPercentage(): number {
if (!this.product.originalPrice) return 0;
return Math.round(
((this.product.originalPrice - this.product.price) / this.product.originalPrice) * 100
);
}
get badgeText(): string {
switch (this.product.badge) {
case 'new': return 'New';
case 'sale': return `${this.discountPercentage}% OFF`;
case 'bestseller': return 'Bestseller';
default: return '';
}
}
get ratingArray(): number[] {
return Array(5).fill(0).map((_, i) => i);
}
onAddToCart(event: Event): void {
event.preventDefault();
if (this.product.inStock) {
this.addToCart.emit(this.product.id);
}
}
onToggleWishlist(event: Event): void {
event.preventDefault();
this.toggleWishlist.emit(this.product.id);
}
onImageLoad(): void {
this.imageLoaded = true;
}
isStarFilled(index: number): boolean {
return index < Math.floor(this.product.rating);
}
}
<!-- product-card.component.html -->
<div
class="product-card variant-{{ variant }}"
(mouseenter)="isHovered = true"
(mouseleave)="isHovered = false"
role="article"
[attr.aria-label]="'Product: ' + product.name"
>
<!-- Badge -->
<div
*ngIf="product.badge"
class="badge badge-{{ product.badge }}"
>
{{ badgeText }}
</div>
<!-- Wishlist Button -->
<button
class="wishlist-btn"
[class.active]="isInWishlist"
(click)="onToggleWishlist($event)"
[attr.aria-label]="isInWishlist ? 'Remove from wishlist' : 'Add to wishlist'"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
[attr.fill]="isInWishlist ? 'currentColor' : 'none'"
stroke="currentColor"
>
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</button>
<!-- Image -->
<div class="image-wrapper">
<div *ngIf="!imageLoaded" class="image-skeleton"></div>
<img
[src]="product.imageUrl"
[alt]="product.name"
[class.loaded]="imageLoaded"
(load)="onImageLoad()"
loading="lazy"
class="image"
/>
</div>
<!-- Content -->
<div class="content">
<h3 class="name">{{ product.name }}</h3>
<p *ngIf="variant === 'detailed'" class="description">
{{ product.description }}
</p>
<!-- Rating -->
<div class="rating">
<div class="stars">
<svg
*ngFor="let i of ratingArray"
width="16"
height="16"
viewBox="0 0 24 24"
[attr.fill]="isStarFilled(i) ? 'currentColor' : 'none'"
stroke="currentColor"
class="star"
>
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>
</div>
<span class="review-count">({{ product.reviewCount }})</span>
</div>
<!-- Price -->
<div class="price-wrapper">
<span class="price">${{ product.price.toFixed(2) }}</span>
<span *ngIf="product.originalPrice" class="original-price">
${{ product.originalPrice.toFixed(2) }}
</span>
</div>
<!-- Stock Status -->
<div *ngIf="!product.inStock" class="out-of-stock">
Out of Stock
</div>
<!-- Add to Cart Button -->
<button
class="add-to-cart-btn"
[class.disabled]="!product.inStock"
(click)="onAddToCart($event)"
[disabled]="!product.inStock"
aria-label="Add to cart"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="9" cy="21" r="1"/>
<circle cx="20" cy="21" r="1"/>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>
</svg>
<span>Add to Cart</span>
</button>
</div>
<!-- Quick View Overlay -->
<div *ngIf="isHovered && variant === 'default'" class="quick-view">
<button class="quick-view-btn">Quick View</button>
</div>
</div>
13.2. Backend: Node.js, Python, Ruby
Express.js API с TypeScript
You: Создай полноценный Express API сервер с аутентификацией
AI:// src/server.ts
import express, { Application } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import compression from 'compression';
import rateLimit from 'express-rate-limit';
import morgan from 'morgan';
import { config } from './config';
import { errorHandler } from './middleware/errorHandler';
import { notFoundHandler } from './middleware/notFoundHandler';
import routes from './routes';
import { connectDatabase } from './database';
import { logger } from './utils/logger';
class Server {
public app: Application;
constructor() {
this.app = express();
this.initializeMiddlewares();
this.initializeRoutes();
this.initializeErrorHandling();
}
private initializeMiddlewares(): void {
// Security
this.app.use(helmet());
this.app.use(cors({
origin: config.cors.origin,
credentials: true
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
this.app.use('/api/', limiter);
// Body parsing
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Compression
this.app.use(compression());
// Logging
this.app.use(morgan('combined', { stream: logger.stream }));
// Health check
this.app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
}
private initializeRoutes(): void {
this.app.use('/api', routes);
}
private initializeErrorHandling(): void {
this.app.use(notFoundHandler);
this.app.use(errorHandler);
}
public async start(): Promise<void> {
try {
// Connect to database
await connectDatabase();
logger.info('Database connected successfully');
// Start server
const PORT = config.port || 3000;
this.app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
logger.info(`Environment: ${config.env}`);
});
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
}
public async stop(): Promise<void> {
logger.info('Shutting down server...');
// Close database connections, etc.
process.exit(0);
}
}
// Graceful shutdown
const server = new Server();
process.on('SIGTERM', async () => {
await server.stop();
});
process.on('SIGINT', async () => {
await server.stop();
});
server.start();
export default server;
// src/routes/auth.routes.ts
import { Router } from 'express';
import { AuthController } from '../controllers/auth.controller';
import { validate } from '../middleware/validate';
import { registerSchema, loginSchema } from '../validators/auth.validator';
import { authenticate } from '../middleware/authenticate';
const router = Router();
const authController = new AuthController();
/**
* @route POST /api/auth/register
* @desc Register new user
* @access Public
*/
router.post(
'/register',
validate(registerSchema),
authController.register.bind(authController)
);
/**
* @route POST /api/auth/login
* @desc Login user
* @access Public
*/
router.post(
'/login',
validate(loginSchema),
authController.login.bind(authController)
);
/**
* @route POST /api/auth/refresh
* @desc Refresh access token
* @access Public
*/
router.post('/refresh', authController.refreshToken.bind(authController));
/**
* @route POST /api/auth/logout
* @desc Logout user
* @access Private
*/
router.post(
'/logout',
authenticate,
authController.logout.bind(authController)
);
/**
* @route GET /api/auth/me
* @desc Get current user
* @access Private
*/
router.get(
'/me',
authenticate,
authController.getCurrentUser.bind(authController)
);
export default router;
// src/controllers/auth.controller.ts
import { Request, Response, NextFunction } from 'express';
import { AuthService } from '../services/auth.service';
import { AppError } from '../utils/AppError';
import { logger } from '../utils/logger';
export class AuthController {
private authService: AuthService;
constructor() {
this.authService = new AuthService();
}
async register(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const { email, password, username } = req.body;
const result = await this.authService.register({
email,
password,
username
});
logger.info(`New user registered: ${email}`);
res.status(201).json({
success: true,
data: result
});
} catch (error) {
next(error);
}
}
async login(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const { email, password } = req.body;
const result = await this.authService.login(email, password);
logger.info(`User logged in: ${email}`);
// Set refresh token in httpOnly cookie
res.cookie('refreshToken', result.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.json({
success: true,
data: {
user: result.user,
accessToken: result.accessToken
}
});
} catch (error) {
next(error);
}
}
async refreshToken(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const { refreshToken } = req.cookies;
if (!refreshToken) {
throw new AppError(401, 'Refresh token not provided');
}
const result = await this.authService.refreshAccessToken(refreshToken);
res.json({
success: true,
data: result
});
} catch (error) {
next(error);
}
}
async logout(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const { refreshToken } = req.cookies;
if (refreshToken) {
await this.authService.logout(refreshToken);
}
res.clearCookie('refreshToken');
res.json({
success: true,
message: 'Logged out successfully'
});
} catch (error) {
next(error);
}
}
async getCurrentUser(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
// User is attached to request by authenticate middleware
const user = (req as any).user;
res.json({
success: true,
data: user
});
} catch (error) {
next(error);
}
}
}
FastAPI (Python) Backend
You: Создай аналогичный API на Python с FastAPI
AI:# main.py
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.security import OAuth2PasswordBearer
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
import uvicorn
from typing import Optional
import logging
from config import settings
from database import engine, Base
from routers import auth, users, posts
from middleware.error_handler import error_handler_middleware
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Create database tables
Base.metadata.create_all(bind=engine)
# Initialize app
app = FastAPI(
title=settings.APP_NAME,
version="1.0.0",
description="API for blog platform"
)
# Rate limiting
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)
# Compression
app.add_middleware(GZipMiddleware, minimum_size=1000)
# Error handling
app.middleware("http")(error_handler_middleware)
# Health check
@app.get("/health")
async def health_check():
return {
"status": "ok",
"version": "1.0.0"
}
# Include routers
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(users.router, prefix="/api/users", tags=["users"])
app.include_router(posts.router, prefix="/api/posts", tags=["posts"])
# Startup event
@app.on_event("startup")
async def startup_event():
logger.info(f"Starting {settings.APP_NAME}")
logger.info(f"Environment: {settings.ENVIRONMENT}")
# Shutdown event
@app.on_event("shutdown")
async def shutdown_event():
logger.info("Shutting down application")
if __name__ == "__main__":
uvicorn.run(
"main:app",
host="0.0.0.0",
port=settings.PORT,
reload=settings.ENVIRONMENT == "development"
)
# routers/auth.py
from fastapi import APIRouter, Depends, HTTPException, status, Response
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from datetime import timedelta
from database import get_db
from schemas.auth import (
UserRegister,
UserLogin,
TokenResponse,
UserResponse
)
from services.auth_service import AuthService
from dependencies.auth import get_current_user
from models.user import User
from config import settings
import logging
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
async def register(
user_data: UserRegister,
db: Session = Depends(get_db)
):
"""
Register a new user
- **email**: Valid email address
- **username**: Unique username
- **password**: Minimum 8 characters
"""
auth_service = AuthService(db)
try:
result = await auth_service.register(
email=user_data.email,
username=user_data.username,
password=user_data.password
)
logger.info(f"New user registered: {user_data.email}")
return result
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
logger.error(f"Registration error: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Registration failed"
)
@router.post("/login", response_model=TokenResponse)
async def login(
response: Response,
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)
):
"""
Login with email and password
Returns access token and refresh token
"""
auth_service = AuthService(db)
try:
result = await auth_service.login(
email=form_data.username, # OAuth2 uses 'username' field
password=form_data.password
)
# Set refresh token in httpOnly cookie
response.set_cookie(
key="refresh_token",
value=result["refresh_token"],
httponly=True,
secure=settings.ENVIRONMENT == "production",
samesite="strict",
max_age=timedelta(days=7).total_seconds()
)
logger.info(f"User logged in: {form_data.username}")
return {
"access_token": result["access_token"],
"refresh_token": result["refresh_token"],
"token_type": "bearer",
"user": result["user"]
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e),
headers={"WWW-Authenticate": "Bearer"}
)
@router.post("/refresh", response_model=dict)
async def refresh_token(
refresh_token: str,
db: Session = Depends(get_db)
):
"""
Refresh access token using refresh token
"""
auth_service = AuthService(db)
try:
new_access_token = await auth_service.refresh_access_token(refresh_token)
return {
"access_token": new_access_token,
"token_type": "bearer"
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e)
)
@router.post("/logout")
async def logout(
response: Response,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
Logout current user
"""
# Clear refresh token cookie
response.delete_cookie("refresh_token")
logger.info(f"User logged out: {current_user.email}")
return {"message": "Successfully logged out"}
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
current_user: User = Depends(get_current_user)
):
"""
Get current authenticated user information
"""
return current_user
Ruby on Rails API
You: Создай RESTful API на Ruby on Rails
AI:# app/controllers/api/v1/auth_controller.rb
module Api
module V1
class AuthController < ApplicationController
skip_before_action :authenticate_request, only: [:register, :login, :refresh]
# POST /api/v1/auth/register
def register
user = User.new(user_params)
if user.save
token = JsonWebToken.encode(user_id: user.id)
refresh_token = generate_refresh_token(user)
Rails.logger.info "New user registered: #{user.email}"
render json: {
success: true,
data: {
user: UserSerializer.new(user).as_json,
access_token: token,
refresh_token: refresh_token
}
}, status: :created
else
render json: {
success: false,
errors: user.errors.full_messages
}, status: :unprocessable_entity
end
rescue StandardError => e
Rails.logger.error "Registration error: #{e.message}"
render json: {
success: false,
error: 'Registration failed'
}, status: :internal_server_error
end
# POST /api/v1/auth/login
def login
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
token = JsonWebToken.encode(user_id: user.id)
refresh_token = generate_refresh_token(user)
# Set refresh token in cookie
cookies.signed[:refresh_token] = {
value: refresh_token,
httponly: true,
secure: Rails.env.production?,
same_site: :strict,
expires: 7.days.from_now
}
Rails.logger.info "User logged in: #{user.email}"
render json: {
success: true,
data: {
user: UserSerializer.new(user).as_json,
access_token: token
}
}
else
render json: {
success: false,
error: 'Invalid credentials'
}, status: :unauthorized
end
end
# POST /api/v1/auth/refresh
def refresh
refresh_token = cookies.signed[:refresh_token] || params[:refresh_token]
if refresh_token.blank?
render json: {
success: false,
error: 'Refresh token not provided'
}, status: :unauthorized
return
end
decoded = JsonWebToken.decode(refresh_token)
user = User.find(decoded[:user_id])
new_token = JsonWebToken.encode(user_id: user.id)
render json: {
success: true,
data: {
access_token: new_token
}
}
rescue ActiveRecord::RecordNotFound
render json: {
success: false,
error: 'Invalid refresh token'
}, status: :unauthorized
rescue JWT::DecodeError
render json: {
success: false,
error: 'Invalid or expired refresh token'
}, status: :unauthorized
end
# POST /api/v1/auth/logout
def logout
cookies.delete(:refresh_token)
Rails.logger.info "User logged out: #{@current_user.email}"
render json: {
success: true,
message: 'Logged out successfully'
}
end
# GET /api/v1/auth/me
def me
render json: {
success: true,
data: UserSerializer.new(@current_user).as_json
}
end
private
def user_params
params.require(:user).permit(:email, :username, :password, :password_confirmation)
end
def generate_refresh_token(user)
JsonWebToken.encode(
{ user_id: user.id },
7.days.from_now
)
end
end
end
end
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
# Validations
validates :email, presence: true, uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :username, presence: true, uniqueness: { case_sensitive: false },
length: { minimum: 3, maximum: 50 }
validates :password, length: { minimum: 8 }, if: :password_required?
# Callbacks
before_save :downcase_email
before_save :downcase_username
# Associations
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy
has_many :likes, dependent: :destroy
# Scopes
scope :active, -> { where(deleted_at: nil) }
scope :verified, -> { where(email_verified: true) }
# Instance methods
def soft_delete
update(deleted_at: Time.current)
end
def restore
update(deleted_at: nil)
end
def deleted?
deleted_at.present?
end
private
def downcase_email
self.email = email.downcase if email.present?
end
def downcase_username
self.username = username.downcase if username.present?
end
def password_required?
new_record? || password.present?
end
end
13.3. Full-stack приложения
Next.js Full-stack App
You: Создай full-stack приложение на Next.js 14 с App Router
AI:// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { prisma } from '@/lib/prisma';
import { auth } from '@/lib/auth';
// Validation schema
const createPostSchema = z.object({
title: z.string().min(1).max(255),
content: z.string().min(1),
excerpt: z.string().max(500).optional(),
categoryIds: z.array(z.string()).optional()
});
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const skip = (page - 1) * limit;
const [posts, total] = await Promise.all([
prisma.post.findMany({
skip,
take: limit,
where: {
status: 'published',
deletedAt: null
},
include: {
author: {
select: {
id: true,
username: true,
avatarUrl: true
}
},
categories: true,
_count: {
select: {
likes: true,
comments: true
}
}
},
orderBy: {
publishedAt: 'desc'
}
}),
prisma.post.count({
where: {
status: 'published',
deletedAt: null
}
})
]);
return NextResponse.json({
success: true,
data: posts,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('Error fetching posts:', error);
return NextResponse.json(
{ success: false, error: 'Failed to fetch posts' },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
// Check authentication
const session = await auth();
if (!session?.user) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
);
}
// Parse and validate request body
const body = await request.json();
const validatedData = createPostSchema.parse(body);
// Create post
const post = await prisma.post.create({
data: {
title: validatedData.title,
content: validatedData.content,
excerpt: validatedData.excerpt,
authorId: session.user.id,
slug: generateSlug(validatedData.title),
status: 'draft',
categories: validatedData.categoryIds ? {
connect: validatedData.categoryIds.map(id => ({ id }))
} : undefined
},
include: {
author: {
select: {
id: true,
username: true,
avatarUrl: true
}
},
categories: true
}
});
return NextResponse.json(
{ success: true, data: post },
{ status: 201 }
);
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, errors: error.errors },
{ status: 400 }
);
}
console.error('Error creating post:', error);
return NextResponse.json(
{ success: false, error: 'Failed to create post' },
{ status: 500 }
);
}
}
function generateSlug(title: string): string {
return title
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
}
// app/(dashboard)/posts/create/page.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import dynamic from 'next/dynamic';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useToast } from '@/components/ui/use-toast';
// Dynamically import rich text editor
const RichTextEditor = dynamic(() => import('@/components/RichTextEditor'), {
ssr: false
});
const createPostSchema = z.object({
title: z.string().min(1, 'Title is required').max(255),
content: z.string().min(1, 'Content is required'),
excerpt: z.string().max(500).optional(),
categoryIds: z.array(z.string()).optional()
});
type CreatePostFormData = z.infer<typeof createPostSchema>;
export default function CreatePostPage() {
const router = useRouter();
const { toast } = useToast();
const [isSubmitting, setIsSubmitting] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
setValue,
watch
} = useForm<CreatePostFormData>({
resolver: zodResolver(createPostSchema)
});
const content = watch('content');
const onSubmit = async (data: CreatePostFormData) => {
setIsSubmitting(true);
try {
const response = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
toast({
title: 'Success',
description: 'Post created successfully'
});
router.push(`/posts/${result.data.id}`);
} else {
toast({
title: 'Error',
description: result.error || 'Failed to create post',
variant: 'destructive'
});
}
} catch (error) {
toast({
title: 'Error',
description: 'An unexpected error occurred',
variant: 'destructive'
});
} finally {
setIsSubmitting(false);
}
};
return (
<div className="container max-w-4xl py-8">
<h1 className="text-3xl font-bold mb-8">Create New Post</h1>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Title */}
<div>
<Label htmlFor="title">Title</Label>
<Input
id="title"
{...register('title')}
placeholder="Enter post title"
className="mt-1"
/>
{errors.title && (
<p className="text-sm text-red-600 mt-1">
{errors.title.message}
</p>
)}
</div>
{/* Excerpt */}
<div>
<Label htmlFor="excerpt">Excerpt (Optional)</Label>
<Input
id="excerpt"
{...register('excerpt')}
placeholder="Brief description of the post"
className="mt-1"
/>
{errors.excerpt && (
<p className="text-sm text-red-600 mt-1">
{errors.excerpt.message}
</p>
)}
</div>
{/* Content */}
<div>
<Label htmlFor="content">Content</Label>
<RichTextEditor
value={content}
onChange={(value) => setValue('content', value)}
className="mt-1"
/>
{errors.content && (
<p className="text-sm text-red-600 mt-1">
{errors.content.message}
</p>
)}
</div>
{/* Actions */}
<div className="flex gap-4">
<Button
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? 'Creating...' : 'Create Post'}
</Button>
<Button
type="button"
variant="outline"
onClick={() => router.back()}
>
Cancel
</Button>
</div>
</form>
</div>
);
}
13.4. API разработка
GraphQL API с Apollo Server
You: Создай GraphQL API с типами, resolvers и subscriptions
AI:// src/graphql/schema.ts
import { gql } from 'apollo-server-express';
export const typeDefs = gql`
scalar DateTime
type User {
id: ID!
email: String!
username: String!
firstName: String
lastName: String
avatarUrl: String
bio: String
posts: [Post!]!
followers: [User!]!
following: [User!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
slug: String!
content: String!
excerpt: String
featuredImage: String
author: User!
categories: [Category!]!
tags: [Tag!]!
comments: [Comment!]!
likes: [Like!]!
likeCount: Int!
commentCount: Int!
viewCount: Int!
status: PostStatus!
publishedAt: DateTime
createdAt: DateTime!
updatedAt: DateTime!
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type Category {
id: ID!
name: String!
slug: String!
description: String
posts: [Post!]!
}
type Tag {
id: ID!
name: String!
slug: String!
posts: [Post!]!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
parent: Comment
replies: [Comment!]!
createdAt: DateTime!
updatedAt: DateTime!
}
type Like {
id: ID!
user: User!
post: Post!
createdAt: DateTime!
}
type AuthPayload {
token: String!
refreshToken: String!
user: User!
}
type PaginatedPosts {
posts: [Post!]!
pageInfo: PageInfo!
}
type PageInfo {
total: Int!
page: Int!
pageSize: Int!
totalPages: Int!
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
input PostFilter {
status: PostStatus
authorId: ID
categoryId: ID
tagId: ID
search: String
}
input CreatePostInput {
title: String!
content: String!
excerpt: String
featuredImage: String
categoryIds: [ID!]
tagIds: [ID!]
status: PostStatus
}
input UpdatePostInput {
title: String
content: String
excerpt: String
featuredImage: String
categoryIds: [ID!]
tagIds: [ID!]
status: PostStatus
}
type Query {
# Users
user(id: ID!): User
users(page: Int, limit: Int): [User!]!
me: User
# Posts
post(id: ID, slug: String): Post
posts(page: Int, limit: Int, filter: PostFilter): PaginatedPosts!
# Categories
categories: [Category!]!
category(id: ID, slug: String): Category
# Tags
tags: [Tag!]!
tag(id: ID, slug: String): Tag
}
type Mutation {
# Auth
register(email: String!, username: String!, password: String!): AuthPayload!
login(email: String!, password: String!): AuthPayload!
refreshToken(refreshToken: String!): AuthPayload!
# Posts
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
publishPost(id: ID!): Post!
unpublishPost(id: ID!): Post!
# Comments
createComment(postId: ID!, content: String!, parentId: ID): Comment!
updateComment(id: ID!, content: String!): Comment!
deleteComment(id: ID!): Boolean!
# Likes
likePost(postId: ID!): Like!
unlikePost(postId: ID!): Boolean!
# Follow
followUser(userId: ID!): User!
unfollowUser(userId: ID!): User!
}
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
postLiked(postId: ID!): Like!
}
`;
// src/graphql/resolvers.ts
import { PubSub } from 'graphql-subscriptions';
import { AuthService } from '../services/auth.service';
import { PostService } from '../services/post.service';
import { UserService } from '../services/user.service';
const pubsub = new PubSub();
// Events
const POST_CREATED = 'POST_CREATED';
const COMMENT_ADDED = 'COMMENT_ADDED';
const POST_LIKED = 'POST_LIKED';
export const resolvers = {
Query: {
me: async (_: any, __: any, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return context.user;
},
user: async (_: any, { id }: any, { dataSources }: any) => {
return dataSources.userService.findById(id);
},
posts: async (_: any, { page = 1, limit = 10, filter }: any, { dataSources }: any) => {
return dataSources.postService.findAll({ page, limit, filter });
},
post: async (_: any, { id, slug }: any, { dataSources }: any) => {
if (id) {
return dataSources.postService.findById(id);
}
if (slug) {
return dataSources.postService.findBySlug(slug);
}
throw new Error('Either id or slug must be provided');
}
},
Mutation: {
register: async (_: any, { email, username, password }: any, { dataSources }: any) => {
return dataSources.authService.register({ email, username, password });
},
login: async (_: any, { email, password }: any, { dataSources }: any) => {
return dataSources.authService.login(email, password);
},
createPost: async (_: any, { input }: any, { user, dataSources }: any) => {
if (!user) {
throw new Error('Not authenticated');
}
const post = await dataSources.postService.create({
...input,
authorId: user.id
});
// Publish to subscribers
pubsub.publish(POST_CREATED, { postCreated: post });
return post;
},
updatePost: async (_: any, { id, input }: any, { user, dataSources }: any) => {
if (!user) {
throw new Error('Not authenticated');
}
const post = await dataSources.postService.findById(id);
if (post.authorId !== user.id) {
throw new Error('Not authorized');
}
return dataSources.postService.update(id, input);
},
deletePost: async (_: any, { id }: any, { user, dataSources }: any) => {
if (!user) {
throw new Error('Not authenticated');
}
const post = await dataSources.postService.findById(id);
if (post.authorId !== user.id) {
throw new Error('Not authorized');
}
await dataSources.postService.delete(id);
return true;
},
likePost: async (_: any, { postId }: any, { user, dataSources }: any) => {
if (!user) {
throw new Error('Not authenticated');
}
const like = await dataSources.postService.like(postId, user.id);
// Publish to subscribers
pubsub.publish(POST_LIKED, { postLiked: like, postId });
return like;
}
},
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator([POST_CREATED])
},
commentAdded: {
subscribe: (_: any, { postId }: any) =>
pubsub.asyncIterator([`${COMMENT_ADDED}_${postId}`])
},
postLiked: {
subscribe: (_: any, { postId }: any) =>
pubsub.asyncIterator([`${POST_LIKED}_${postId}`])
}
},
// Field resolvers
Post: {
author: async (parent: any, _: any, { dataSources }: any) => {
return dataSources.userService.findById(parent.authorId);
},
categories: async (parent: any, _: any, { dataSources }: any) => {
return dataSources.postService.getCategories(parent.id);
},
tags: async (parent: any, _: any, { dataSources }: any) => {
return dataSources.postService.getTags(parent.id);
},
comments: async (parent: any, _: any, { dataSources }: any) => {
return dataSources.postService.getComments(parent.id);
},
likeCount: async (parent: any, _: any, { dataSources }: any) => {
return dataSources.postService.getLikeCount(parent.id);
},
commentCount: async (parent: any, _: any, { dataSources }: any) => {
return dataSources.postService.getCommentCount(parent.id);
}
},
User: {
posts: async (parent: any, _: any, { dataSources }: any) => {
return dataSources.postService.findByAuthor(parent.id);
},
followers: async (parent: any, _: any, { dataSources }: any) => {
return dataSources.userService.getFollowers(parent.id);
},
following: async (parent: any, _: any, { dataSources }: any) => {
return dataSources.userService.getFollowing(parent.id);
}
}
};
13.5. Работа с базами данных
Prisma ORM Setup
You: Создай Prisma schema для блог-платформы
AI:// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
username String @unique
passwordHash String @map("password_hash")
firstName String? @map("first_name")
lastName String? @map("last_name")
bio String?
avatarUrl String? @map("avatar_url")
emailVerified Boolean @default(false) @map("email_verified")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
// Relations
posts Post[]
comments Comment[]
likes Like[]
followers Follow[] @relation("Following")
following Follow[] @relation("Followers")
refreshTokens RefreshToken[]
@@map("users")
}
model RefreshToken {
id String @id @default(uuid())
token String @unique
userId String @map("user_id")
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("refresh_tokens")
}
model Post {
id String @id @default(uuid())
title String
slug String @unique
content String
excerpt String?
featuredImage String? @map("featured_image")
authorId String @map("author_id")
status PostStatus @default(DRAFT)
viewCount Int @default(0) @map("view_count")
publishedAt DateTime? @map("published_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
// Relations
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
categories Category[] @relation("PostCategories")
tags Tag[] @relation("PostTags")
comments Comment[]
likes Like[]
@@index([authorId])
@@index([status])
@@index([publishedAt])
@@map("posts")
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
model Category {
id String @id @default(uuid())
name String @unique
slug String @unique
description String?
createdAt DateTime @default(now()) @map("created_at")
posts Post[] @relation("PostCategories")
@@map("categories")
}
model Tag {
id String @id @default(uuid())
name String @unique
slug String @unique
createdAt DateTime @default(now()) @map("created_at")
posts Post[] @relation("PostTags")
@@map("tags")
}
model Comment {
id String @id @default(uuid())
content String
postId String @map("post_id")
authorId String @map("author_id")
parentId String? @map("parent_id")
status CommentStatus @default(PENDING)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
// Relations
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id])
replies Comment[] @relation("CommentReplies")
@@index([postId])
@@index([authorId])
@@map("comments")
}
enum CommentStatus {
PENDING
APPROVED
SPAM
}
model Like {
id String @id @default(uuid())
userId String @map("user_id")
postId String @map("post_id")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@unique([userId, postId])
@@index([postId])
@@map("likes")
}
model Follow {
followerId String @map("follower_id")
followingId String @map("following_id")
createdAt DateTime @default(now()) @map("created_at")
follower User @relation("Followers", fields: [followerId], references: [id], onDelete: Cascade)
following User @relation("Following", fields: [followingId], references: [id], onDelete: Cascade)
@@id([followerId, followingId])
@@index([followingId])
@@map("follows")
}