Projeto E-commerce - Parte 1
Vamos construir uma API de e-commerce completa aplicando tudo que aprendemos.
Estrutura do Projeto
src/
├── config/
│ ├── database.ts
│ ├── redis.ts
│ ├── mail.ts
│ └── swagger.ts
├── modules/
│ ├── auth/
│ │ ├── auth.controller.ts
│ │ ├── auth.service.ts
│ │ ├── auth.routes.ts
│ │ └── auth.schema.ts
│ ├── users/
│ ├── products/
│ ├── categories/
│ ├── cart/
│ ├── orders/
│ └── payments/
├── shared/
│ ├── middlewares/
│ ├── errors/
│ └── utils/
├── app.ts
└── server.ts
Schema do Banco (Prisma)
prisma
// prisma/schema.prismagenerator client { provider = "prisma-client-js"} datasource db { provider = "postgresql" url = env("DATABASE_URL")} enum Role { USER ADMIN} enum OrderStatus { PENDING PAID SHIPPED DELIVERED CANCELLED} model User { id String @id @default(uuid()) email String @unique password String name String role Role @default(USER) avatar String? addresses Address[] orders Order[] reviews Review[] cart Cart? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt} model Address { id String @id @default(uuid()) user User @relation(fields: [userId], references: [id]) userId String street String number String complement String? city String state String zipCode String isDefault Boolean @default(false)} model Category { id String @id @default(uuid()) name String @unique slug String @unique description String? image String? products Product[]} model Product { id String @id @default(uuid()) name String slug String @unique description String price Decimal @db.Decimal(10, 2) comparePrice Decimal? @db.Decimal(10, 2) stock Int @default(0) images String[] featured Boolean @default(false) active Boolean @default(true) category Category @relation(fields: [categoryId], references: [id]) categoryId String reviews Review[] cartItems CartItem[] orderItems OrderItem[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt} model Review { id String @id @default(uuid()) rating Int comment String user User @relation(fields: [userId], references: [id]) userId String product Product @relation(fields: [productId], references: [id]) productId String createdAt DateTime @default(now()) @@unique([userId, productId])} model Cart { id String @id @default(uuid()) user User @relation(fields: [userId], references: [id]) userId String @unique items CartItem[] updatedAt DateTime @updatedAt} model CartItem { id String @id @default(uuid()) cart Cart @relation(fields: [cartId], references: [id], onDelete: Cascade) cartId String product Product @relation(fields: [productId], references: [id]) productId String quantity Int @@unique([cartId, productId])} model Order { id String @id @default(uuid()) user User @relation(fields: [userId], references: [id]) userId String items OrderItem[] status OrderStatus @default(PENDING) subtotal Decimal @db.Decimal(10, 2) shipping Decimal @db.Decimal(10, 2) discount Decimal @db.Decimal(10, 2) @default(0) total Decimal @db.Decimal(10, 2) paymentMethod String paymentId String? shippingAddress Json notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt} model OrderItem { id String @id @default(uuid()) order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) orderId String product Product @relation(fields: [productId], references: [id]) productId String name String price Decimal @db.Decimal(10, 2) quantity Int} model Coupon { id String @id @default(uuid()) code String @unique discountType String // PERCENTAGE or FIXED discountValue Decimal @db.Decimal(10, 2) minPurchase Decimal? @db.Decimal(10, 2) maxUses Int? usedCount Int @default(0) expiresAt DateTime? active Boolean @default(true)}Configuração Base
typescript
// src/app.tsimport express from 'express';import helmet from 'helmet';import cors from 'cors';import swaggerUi from 'swagger-ui-express'; import { errorHandler } from './shared/middlewares/error.middleware';import { rateLimiter } from './shared/middlewares/rate-limit.middleware';import { swaggerSpec } from './config/swagger'; import authRoutes from './modules/auth/auth.routes';import userRoutes from './modules/users/user.routes';import categoryRoutes from './modules/categories/category.routes';import productRoutes from './modules/products/product.routes';import cartRoutes from './modules/cart/cart.routes';import orderRoutes from './modules/orders/order.routes';import healthRoutes from './shared/routes/health.routes'; const app = express(); // Middlewares globaisapp.use(helmet());app.use(cors());app.use(express.json());app.use(rateLimiter); // Documentaçãoapp.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); // Rotasapp.use('/health', healthRoutes);app.use('/api/auth', authRoutes);app.use('/api/users', userRoutes);app.use('/api/categories', categoryRoutes);app.use('/api/products', productRoutes);app.use('/api/cart', cartRoutes);app.use('/api/orders', orderRoutes); // Error handlerapp.use(errorHandler); export { app };Módulo de Autenticação
typescript
// src/modules/auth/auth.service.tsimport bcrypt from 'bcryptjs';import jwt from 'jsonwebtoken';import { prisma } from '../../config/database';import { AppError } from '../../shared/errors/AppError';import { RegisterDTO, LoginDTO } from './auth.schema'; export class AuthService { async register(data: RegisterDTO) { const exists = await prisma.user.findUnique({ where: { email: data.email }, }); if (exists) { throw new AppError('E-mail já cadastrado', 400); } const hashedPassword = await bcrypt.hash(data.password, 12); const user = await prisma.user.create({ data: { ...data, password: hashedPassword, }, select: { id: true, email: true, name: true, role: true, }, }); // Cria carrinho vazio await prisma.cart.create({ data: { userId: user.id }, }); const token = this.generateToken(user.id); return { user, token }; } async login(data: LoginDTO) { const user = await prisma.user.findUnique({ where: { email: data.email }, }); if (!user) { throw new AppError('Credenciais inválidas', 401); } const isValid = await bcrypt.compare(data.password, user.password); if (!isValid) { throw new AppError('Credenciais inválidas', 401); } const token = this.generateToken(user.id); return { user: { id: user.id, email: user.email, name: user.name, role: user.role, }, token, }; } private generateToken(userId: string): string { return jwt.sign({ userId }, process.env.JWT_SECRET!, { expiresIn: '7d', }); }}Módulo de Produtos
typescript
// src/modules/products/product.service.tsimport { prisma } from '../../config/database';import { CacheService } from '../../shared/services/cache.service';import { AppError } from '../../shared/errors/AppError'; const cache = new CacheService();const CACHE_TTL = 600; // 10 minutos interface FindAllParams { page?: number; limit?: number; category?: string; search?: string; minPrice?: number; maxPrice?: number; sortBy?: 'price' | 'name' | 'createdAt'; order?: 'asc' | 'desc';} export class ProductService { async findAll(params: FindAllParams) { const { page = 1, limit = 20, category, search, minPrice, maxPrice, sortBy = 'createdAt', order = 'desc', } = params; const where: any = { active: true }; if (category) { where.category = { slug: category }; } if (search) { where.OR = [ { name: { contains: search, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } }, ]; } if (minPrice || maxPrice) { where.price = {}; if (minPrice) where.price.gte = minPrice; if (maxPrice) where.price.lte = maxPrice; } const skip = (page - 1) * limit; const [products, total] = await Promise.all([ prisma.product.findMany({ where, skip, take: limit, orderBy: { [sortBy]: order }, include: { category: { select: { id: true, name: true, slug: true } }, }, }), prisma.product.count({ where }), ]); return { data: products, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; } async findBySlug(slug: string) { return cache.getOrSet(`product:${slug}`, async () => { const product = await prisma.product.findUnique({ where: { slug }, include: { category: true, reviews: { include: { user: { select: { name: true } } }, orderBy: { createdAt: 'desc' }, take: 10, }, }, }); if (!product) { throw new AppError('Produto não encontrado', 404); } return product; }, CACHE_TTL); }}Resumo
Nesta parte configuramos:
- ✅ Estrutura modular do projeto
- ✅ Schema completo do banco
- ✅ App Express configurado
- ✅ Módulo de autenticação
- ✅ Módulo de produtos com filtros
Próxima aula: Projeto E-commerce - Parte 2! 🛒