1. Главная
  2. Docs
  3. Документация по инструментам и технологиям
  4. Cursor
  5. Web-разработка в Cursor. Практическое применение | Глава 13

Web-разработка в Cursor. Практическое применение | Глава 13

18 октября 2025
83

ЧАСТЬ 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")
}