# .NET + Next.js Stack

Stack full-stack com .NET Web API no backend e Next.js no frontend.

## Visão Geral

| Aspecto | Tecnologia |
|---------|------------|
| **Backend** | .NET 10 Web API / C# 14 |
| **Frontend** | Next.js 15+ / React 19 |
| **Database** | Entity Framework Core 10 + Azure SQL |
| **API** | REST + OpenAPI |
| **Hosting** | Azure Container Apps |

## Triggers

Keywords: `nextjs`, `next.js`, `react`, `api`, `frontend`, `spa`, `typescript`

## Estrutura de Projeto

```
/
├── backend/                       # .NET Web API
│   ├── src/
│   │   ├── Api/
│   │   │   ├── Program.cs
│   │   │   ├── Controllers/
│   │   │   └── Endpoints/         # Minimal APIs
│   │   ├── Application/
│   │   │   ├── Services/
│   │   │   └── DTOs/
│   │   ├── Domain/
│   │   │   └── Entities/
│   │   └── Infrastructure/
│   │       └── Data/
│   ├── tests/
│   └── Backend.sln
│
├── frontend/                      # Next.js
│   ├── src/
│   │   ├── app/                   # App Router
│   │   │   ├── layout.tsx
│   │   │   ├── page.tsx
│   │   │   └── (routes)/
│   │   ├── components/
│   │   ├── lib/
│   │   │   ├── api.ts             # API client
│   │   │   └── types.ts           # Generated from OpenAPI
│   │   └── hooks/
│   ├── package.json
│   └── next.config.js
│
├── docker-compose.yml
└── infra/
    └── main.bicep
```

## Backend (.NET API)

### Program.cs (Minimal API)

```csharp
var builder = WebApplication.CreateBuilder(args);

// OpenAPI
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new() { Title = "My API", Version = "v1" });
});

// CORS para Next.js
builder.Services.AddCors(options =>
{
    options.AddPolicy("Frontend", policy =>
    {
        policy.WithOrigins(builder.Configuration["Frontend:Url"]!)
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials();
    });
});

// Database
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

// Services
builder.Services.AddScoped<IOrderService, OrderService>();

var app = builder.Build();

// Swagger (dev only)
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseCors("Frontend");
app.UseAuthentication();
app.UseAuthorization();

// Map endpoints
app.MapOrderEndpoints();

app.Run();
```

### Minimal API Endpoints

```csharp
// Endpoints/OrderEndpoints.cs
public static class OrderEndpoints
{
    public static void MapOrderEndpoints(this WebApplication app)
    {
        var group = app.MapGroup("/api/orders")
            .WithTags("Orders")
            .RequireAuthorization();

        group.MapGet("/", GetAll);
        group.MapGet("/{id:int}", GetById);
        group.MapPost("/", Create);
        group.MapPut("/{id:int}", Update);
        group.MapDelete("/{id:int}", Delete);
    }

    private static async Task<IResult> GetAll(
        IOrderService service,
        [AsParameters] PaginationQuery query)
    {
        var result = await service.GetAllAsync(query);
        return Results.Ok(result);
    }

    private static async Task<IResult> GetById(
        int id,
        IOrderService service)
    {
        var order = await service.GetByIdAsync(id);
        return order is null ? Results.NotFound() : Results.Ok(order);
    }

    private static async Task<IResult> Create(
        CreateOrderRequest request,
        IOrderService service)
    {
        var order = await service.CreateAsync(request);
        return Results.Created($"/api/orders/{order.Id}", order);
    }

    private static async Task<IResult> Update(
        int id,
        UpdateOrderRequest request,
        IOrderService service)
    {
        await service.UpdateAsync(id, request);
        return Results.NoContent();
    }

    private static async Task<IResult> Delete(
        int id,
        IOrderService service)
    {
        await service.DeleteAsync(id);
        return Results.NoContent();
    }
}
```

## Frontend (Next.js)

### API Client

```typescript
// lib/api.ts
const API_URL = process.env.NEXT_PUBLIC_API_URL!;

async function fetchApi<T>(
  endpoint: string,
  options?: RequestInit
): Promise<T> {
  const res = await fetch(`${API_URL}${endpoint}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...options?.headers,
    },
    credentials: 'include',
  });

  if (!res.ok) {
    throw new Error(`API error: ${res.status}`);
  }

  return res.json();
}

export const api = {
  orders: {
    getAll: (params?: { page?: number; pageSize?: number }) =>
      fetchApi<PaginatedResult<Order>>(`/api/orders?${new URLSearchParams(params as any)}`),

    getById: (id: number) =>
      fetchApi<Order>(`/api/orders/${id}`),

    create: (data: CreateOrderRequest) =>
      fetchApi<Order>('/api/orders', {
        method: 'POST',
        body: JSON.stringify(data),
      }),

    update: (id: number, data: UpdateOrderRequest) =>
      fetchApi<void>(`/api/orders/${id}`, {
        method: 'PUT',
        body: JSON.stringify(data),
      }),

    delete: (id: number) =>
      fetchApi<void>(`/api/orders/${id}`, {
        method: 'DELETE',
      }),
  },
};
```

### Types (gerado do OpenAPI)

```typescript
// lib/types.ts
export interface Order {
  id: number;
  orderNumber: string;
  customerId: number;
  customerName: string;
  total: number;
  status: OrderStatus;
  createdAt: string;
}

export type OrderStatus = 'Pending' | 'Processing' | 'Completed' | 'Cancelled';

export interface CreateOrderRequest {
  customerId: number;
  items: OrderItem[];
  notes?: string;
}

export interface PaginatedResult<T> {
  items: T[];
  totalCount: number;
  page: number;
  pageSize: number;
  totalPages: number;
}
```

### Server Component (App Router)

```tsx
// app/orders/page.tsx
import { api } from '@/lib/api';
import { OrderList } from '@/components/orders/OrderList';

export default async function OrdersPage() {
  const orders = await api.orders.getAll();

  return (
    <div className="container mx-auto p-4">
      <div className="flex justify-between items-center mb-6">
        <h1 className="text-2xl font-bold">Pedidos</h1>
        <a href="/orders/new" className="btn btn-primary">
          Novo Pedido
        </a>
      </div>

      <OrderList initialOrders={orders} />
    </div>
  );
}
```

### Client Component

```tsx
'use client';

// components/orders/OrderList.tsx
import { useState } from 'react';
import { Order, PaginatedResult } from '@/lib/types';
import { api } from '@/lib/api';

interface Props {
  initialOrders: PaginatedResult<Order>;
}

export function OrderList({ initialOrders }: Props) {
  const [orders, setOrders] = useState(initialOrders);
  const [loading, setLoading] = useState(false);

  async function loadPage(page: number) {
    setLoading(true);
    try {
      const result = await api.orders.getAll({ page });
      setOrders(result);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div>
      <table className="w-full">
        <thead>
          <tr>
            <th>Número</th>
            <th>Cliente</th>
            <th>Total</th>
            <th>Status</th>
          </tr>
        </thead>
        <tbody>
          {orders.items.map((order) => (
            <tr key={order.id}>
              <td>{order.orderNumber}</td>
              <td>{order.customerName}</td>
              <td>{order.total.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}</td>
              <td><StatusBadge status={order.status} /></td>
            </tr>
          ))}
        </tbody>
      </table>

      <Pagination
        currentPage={orders.page}
        totalPages={orders.totalPages}
        onPageChange={loadPage}
        disabled={loading}
      />
    </div>
  );
}
```

## Docker Compose

```yaml
# docker-compose.yml
services:
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    ports:
      - "5000:8080"
    environment:
      - ConnectionStrings__Default=Server=db;Database=App;User=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=true
      - Frontend__Url=http://localhost:3000
    depends_on:
      - db

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NEXT_PUBLIC_API_URL=http://localhost:5000

  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=YourStrong!Passw0rd
    ports:
      - "1433:1433"
```

## Documentação de Referência

- [Next.js Documentation](https://nextjs.org/docs)
- [ASP.NET Web API](https://learn.microsoft.com/en-us/aspnet/core/web-api/)
- [React Documentation](https://react.dev/)
- [Minimal APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis)

## Checklist de Projeto

- [ ] Backend com Minimal APIs
- [ ] OpenAPI/Swagger configurado
- [ ] CORS configurado para frontend
- [ ] Frontend com App Router (Next.js 15+)
- [ ] API client tipado
- [ ] Types gerados do OpenAPI
- [ ] Docker Compose para dev
- [ ] Environment variables configuradas

---

*MORPH-SPEC by Polymorphism Tech*
