# TSDIAPI Server

A modern, **ESM-based**, modular TypeScript server built on **Fastify** and **TypeBox**.  
Designed for **performance**, **flexibility**, and **extensibility**, it provides a solid foundation for API development with minimal complexity.

**Version**: `0.4.7` | **API Version**: `v1` | **Node.js**: `20.x`

📚 **Documentation**: For detailed documentation, visit [https://tsdiapi.com/](https://tsdiapi.com/)


## 🚀 Quick Start

1. **Create a new project using CLI**:

   ```bash
   npx @tsdiapi/cli create app
   ```

2. **Navigate into the project**:

   ```bash
   cd app
   ```

3. **Start the server**:
   ```bash
   npm run dev
   ```

Your API is now running! 🎉 Open the browser and check **Swagger UI** at:
👉 `http://localhost:3000/docs`

---

## 📝 Schema Registration

**All schemas must be registered with `addSchema()` and have a unique `$id`:**

```ts
import { addSchema, Type } from "@tsdiapi/server";

const UserSchema = addSchema(
  Type.Object({ name: Type.String() }, { $id: 'UserSchema' })
);
```

### Working with Dates

Use the `DateString` helper for date fields:

```ts
import { DateString } from "@tsdiapi/server";

const EventSchema = addSchema(
  Type.Object({
    title: Type.String(),
    date: DateString() // ISO 8601 date string
  }, { $id: 'EventSchema' })
);
```

### Working with Enums

**Use string enum types instead of Type.Union()** for proper Swagger support and code generation:

```ts
// ✅ Correct - String with enum constraint
const UserSchema = addSchema(
  Type.Object({
    role: Type.String({ enum: ['admin', 'user', 'guest'] })
  }, { $id: 'UserSchema' })
);

// ❌ Avoid - Union types don't work well with Swagger/codegen
const UserSchema = addSchema(
  Type.Object({
    role: Type.Union([
      Type.Literal('admin'),
      Type.Literal('user'),
      Type.Literal('guest')
    ])
  }, { $id: 'UserSchema' })
);
```

**Migrating from v0.3.x?** Enable legacy mode: `legacyAutoSchemaRegistration: true`

---

## 🛠 Features

✅ **Fastify Core** – High-performance HTTP server  
✅ **ESM Support** – Modern ECMAScript modules
✅ **TypeScript** – Strongly-typed language
✅ **TypeBox Validation** – Type-safe request validation  
✅ **Modular Structure** – Auto-load controllers and services  
✅ **Swagger API Docs** – Auto-generated OpenAPI documentation  
✅ **CORS & Security** – Preconfigured CORS & Helmet support  
✅ **File Upload Support** – Handles multipart/form-data  
✅ **Plugin System** – Easily extend with plugins
✅ **Prisma Integration** – Seamless database integration with TypeBox schemas
✅ **Request Context** – Request-scoped storage for sharing data across handlers

---

## 📦 Database Integration

**Prisma ORM** integration with type-safe database access and auto-generated TypeBox schemas.

👉 [Prisma Integration Guide](./readme.prisma.md)

---

## 🎯 Defining API Routes

### Basic Route Example
```ts
import { AppContext, addSchema, Type } from "@tsdiapi/server";

// ✅ Register schemas with addSchema() - REQUIRED!
const UserParamsSchema = addSchema(
  Type.Object({ id: Type.String() }, { $id: 'UserParamsSchema' })
);

const UserResponseSchema = addSchema(
  Type.Object({ id: Type.String(), name: Type.String() }, { $id: 'UserResponseSchema' })
);

export default function userController({ useRoute }: AppContext) {
  useRoute()
    .get("/users/:id")
    .params(UserParamsSchema)
    .code(200, UserResponseSchema)
    .handler(async (req) => {
      return {
        status: 200,
        data: { id: req.params.id, name: "John Doe" },
      };
    })
    .build();
}
```

### CRUD Example
```ts
const CreateUserSchema = addSchema(
  Type.Object({
    name: Type.String(),
    email: Type.String({ format: "email" })
  }, { $id: 'CreateUserSchema' })
);

export default function userController({ useRoute }: AppContext) {
  // Create
  useRoute()
    .post("/users")
    .body(CreateUserSchema)
    .handler(async (req) => {
      const user = await createUser(req.body);
      return { status: 201, data: user };
    })
    .build();

  // Read
  useRoute()
    .get("/users/:id")
    .params(addSchema(Type.Object({ id: Type.String() }, { $id: 'UserParams' })))
    .handler(async (req) => {
      const user = await getUser(req.params.id);
      if (!user) return { status: 404, data: { error: "Not found" } };
      return { status: 200, data: user };
    })
    .build();
}
```

### Protected Route
```ts
useRoute()
  .get("/admin/dashboard")
  .auth("bearer", async (req) => {
    if (!await validateToken(req.headers.authorization)) {
      return { status: 401, data: { error: "Invalid token" } };
    }
    return true;
  })
  .handler(async (req) => {
    return { status: 200, data: await getDashboardStats() };
  })
  .build();
```

### File Upload
```ts
useRoute()
  .post("/upload")
  .acceptMultipart()
  .fileOptions({ maxFileSize: 1024 * 1024 * 5 }, "file")
  .handler(async (req) => {
    const url = await uploadFile(req.body.file);
    return { status: 200, data: { url } };
  })
  .build();
```

👉 [Full Routing Documentation](./readme.routing.md)

---

## 🔐 Authentication Example

Complete authentication system using [@tsdiapi/jwt-auth](https://www.npmjs.com/package/@tsdiapi/jwt-auth):

### Setup

```bash
npm install @tsdiapi/jwt-auth @fastify/cookie @fastify/session
```

```ts
import { createApp } from "@tsdiapi/server";
import createJWTPlugin from "@tsdiapi/jwt-auth";

await createApp({
  plugins: [
    createJWTPlugin({
      authMode: 'hybrid', // Support both JWT and cookies
      guards: {
        admin: (session) => session.role === 'admin',
        user: (session) => session.role === 'user'
      },
      session: {
        store: 'memory',
        secret: process.env.SESSION_SECRET,
        cookieName: 'sid',
        cookieOptions: {
          httpOnly: true,
          secure: process.env.NODE_ENV === 'production',
          maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
        }
      }
    })
  ]
});
```

### Login Route

```ts
import { createHybridAuth } from "@tsdiapi/jwt-auth";

const LoginSchema = addSchema(
  Type.Object({
    email: Type.String({ format: 'email' }),
    password: Type.String()
  }, { $id: 'LoginSchema' })
);

useRoute()
  .post("/auth/login")
  .body(LoginSchema)
  .code(200, addSchema(Type.Object({
    accessToken: Type.String(),
    expiresAt: Type.String()
  }, { $id: 'LoginResponse' })))
  .handler(async (req, reply) => {
    // Validate credentials
    const user = await validateUser(req.body.email, req.body.password);

    // Create JWT + Cookie session
    const { tokens } = await createHybridAuth(req, reply, {
      userId: user.id,
      role: user.role,
      email: user.email
    });

    return {
      status: 200,
      data: {
        accessToken: tokens.accessToken,
        expiresAt: tokens.accessTokenExpiresAt.toISOString()
      }
    };
  })
  .build();
```

### Protected Route

```ts
import { HybridAuthGuard, useSession } from "@tsdiapi/jwt-auth";

useRoute()
  .get("/profile")
  .code(200, addSchema(Type.Object({
    userId: Type.String(),
    email: Type.String(),
    role: Type.String()
  }, { $id: 'ProfileResponse' })))
  .code(403, addSchema(Type.Object({
    error: Type.String()
  }, { $id: 'ErrorResponse' })))
  .guard(HybridAuthGuard({ mode: 'hybrid' }))
  .handler(async (req) => {
    const session = useSession(req);
    return { status: 200, data: session };
  })
  .build();
```

### Admin-Only Route

```ts
useRoute()
  .get("/admin/users")
  .code(200, addSchema(Type.Array(Type.Object({
    id: Type.String(),
    email: Type.String()
  })), { $id: 'UsersListResponse' }))
  .code(403, addSchema(Type.Object({
    error: Type.String()
  }, { $id: 'ErrorResponse' })))
  .guard(HybridAuthGuard({
    mode: 'hybrid',
    guardName: 'admin'
  }))
  .handler(async (req) => {
    const users = await getAllUsers();
    return { status: 200, data: users };
  })
  .build();
```

### Logout Route

```ts
import { destroyUserSession } from "@tsdiapi/jwt-auth";

useRoute()
  .post("/auth/logout")
  .code(200, addSchema(Type.Object({
    message: Type.String()
  }, { $id: 'LogoutResponse' })))
  .handler(async (req, reply) => {
    await destroyUserSession(req, reply);
    return { status: 200, data: { message: 'Logged out' } };
  })
  .build();
```

---

## 📦 Request Context

Share data across handlers and services using request-scoped storage:

```ts
import { getRequestContextValue, setRequestContextValue } from "@tsdiapi/server";

export default function userController({ useRoute }: AppContext) {
  useRoute()
    .get("/users/me")
    .guard(async (req) => {
      const user = await authenticateUser(req.headers.authorization);
      if (!user) return { status: 401, data: { error: "Unauthorized" } };

      setRequestContextValue("user", user); // Store in context
      return true;
    })
    .handler(async (req) => {
      const user = getRequestContextValue("user"); // Retrieve from context
      return { status: 200, data: user };
    })
    .build();
}
```

👉 [Full Documentation](./readme.request-context.md)

---

## ⚙️ Configuration

Auto-loads `.env` variables:

```env
PORT=3000
HOST=localhost
```

## 🔌 Plugins

```ts
import { createApp } from "@tsdiapi/server";
import prismaPlugin from "@tsdiapi/prisma";

await createApp({
  plugins: [prismaPlugin()]
});
```

## 📌 Version

```ts
import { VERSION, API_VERSION } from "@tsdiapi/server";

console.log(VERSION);      // "0.4.7"
console.log(API_VERSION);  // "v1"
```

---

## 📚 Documentation

[Configuration](./readme.createapp.md) | [Routing](./readme.routing.md) | [Prisma](./readme.prisma.md) | [Request Context](./readme.request-context.md)

## 🛠 Commands

```bash
npx @tsdiapi/cli create app  # Create project
npm run dev                  # Start server
npm run build                # Build project
```

## 📜 License

MIT Licensed | Contributions welcome 🚀
