Pular para o conteúdoPedro Farbo
Lição 23 / 2560 min

Projeto E-commerce: Estrutura e Usuários

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! 🛒

Gostou do conteúdo? Sua contribuição ajuda a manter tudo online e gratuito!

PIX:0737160d-e98f-4a65-8392-5dba70e7ff3e