Skip to content

Repository files navigation

πŸš€ WynkJS

A high-performance TypeScript first framework built for Bun with Elegant Decorator-Based Architecture

npm version License: MIT TypeScript Bun

10x faster than Express/NestJs, built for modern TypeScript development for Bun ⚑

Quick Start β€’ CLI Tools β€’ Features β€’ Documentation


About

WynkJS is a modern, TypeScript-first web framework alternative to NestJs that brings Elegant Decorator-Based Architecture to the blazing-fast Elysia runtime built for Bun. It features familiar conceptsβ€”Controllers, Dependency Injection, Guards, Pipes, Interceptors, Plugins, and Exception Filtersβ€”designed for building high‑performance REST APIs and backends on Bun. WynkJS embraces ESM, ships first-class types, and keeps things simple so you can move fast without the bloat.

Keywords: Bun framework, Fast web server, TypeScript decorators, dependency injection (DI), guards, pipes, interceptors, exception filters, fast web framework, REST API, backend, modern TypeScript.


✨ Why WynkJS?

WynkJS combines the speed of Elysia with an Elegant Decorator-Based Architecture, giving you the best of both worlds:

  • πŸš€ 10x Faster - One of the fastest web frameworks for Bun
  • 🎨 Decorator-Based - Clean, intuitive decorator syntax for TypeScript
  • πŸ’‰ Dependency Injection - Built-in DI (no need to import reflect-metadata!)
  • πŸ”’ TypeScript First - TypeScript is mandatory, not optional. Full type safety and IntelliSense support
  • 🎯 Simple & Clean - Easy to learn, powerful to use
  • πŸ”Œ Plugin System - Compression, custom middleware via app.use()
  • πŸ›‘οΈ Middleware Support - Guards, interceptors, pipes, filters
  • ⚑ Bun Only - Built exclusively for Bun runtime (not Node.js)
  • πŸ“¦ Single Import - Everything from wynkjs (Injectable, Controller, Get, etc.)

πŸš€ Get Started in 30 Seconds:

πŸ“¦ create-wynkjs - Project Scaffolding

Quickly scaffold a new WynkJS project with best practices:

# Create a new project
bunx create-wynkjs
# or
npx create-wynkjs

What you get:

  • βœ… TypeScript - Strict mode with decorators enabled (mandatory)
  • βœ… ESLint - Code linting with TypeScript rules (optional)
  • βœ… Prettier - Code formatting (optional)
  • βœ… Husky - Git hooks for pre-commit checks (optional)
  • βœ… Hot Reload - bun --watch for instant feedback
  • βœ… Working Example - Complete CRUD controller, service, and DTOs

Generated Project Structure:

my-wynkjs-app/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ modules/
β”‚   β”‚   └── user/
β”‚   β”‚       β”œβ”€β”€ user.controller.ts
β”‚   β”‚       β”œβ”€β”€ user.service.ts
β”‚   β”‚       └── user.dto.ts
β”‚   └── index.ts
β”œβ”€β”€ .eslintrc.json
β”œβ”€β”€ .prettierrc
β”œβ”€β”€ tsconfig.json
└── package.json

Available Scripts:

  • bun run dev - Development with hot reload
  • bun run start - Production server
  • bun run build - Build TypeScript
  • bun run lint - Run ESLint
  • bun run format - Format with Prettier

Learn more about create-wynkjs

Example:

bunx create-wynkjs
# Choose project name: my-api
# Add ESLint? Yes
# Add Prettier? Yes
# Add Husky? No

cd my-api
bun run dev
# πŸš€ Server running on http://localhost:3000

1. Find Your DTOs (Data Transfer Objects)

// user.dto.ts
import { DTO, CommonDTO } from "wynkjs";

export const CreateUserDTO = DTO.Strict({
  name: DTO.Optional(
    DTO.String({
      maxLength: 50,
      error: "Name must be between 2 and 50 characters",
    })
  ),
  email: CommonDTO.Email({
    description: "User email address",
    error: "Please provide a valid email address",
  }),
  mobile: DTO.Optional(
    DTO.String({
      pattern: "^[6-9]{1}[0-9]{9}$",
      error: "Invalid mobile number format",
    })
  ),
  age: DTO.Optional(
    DTO.Number({
      minimum: 18,
      error: "Age must be at least 18 years",
    })
  ),
});

export interface CreateUserType {
  name?: string;
  email?: string;
  mobile?: string;
  age?: number;
}

export const UserIdDto = DTO.Object({
  id: DTO.String({ minLength: 2, maxLength: 50 }),
});

export interface ParamIdType {
  id: string;
}

2. Find Your Service with Dependency Injection

// email.service.ts
import { Injectable } from "wynkjs";

@Injectable()
export class EmailService {
  async sendWelcomeEmail(email: string, userName: string): Promise<void> {
    console.log(`πŸ“§ Sending welcome email to ${email}`);
    // Your email logic here
  }
}

3. Find Your Controller

// user.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Param,
  Patch,
  Query,
  Injectable,
  NotFoundException,
} from "wynkjs";
import { CreateUserDTO, UserIdDto } from "./user.dto";
import type { CreateUserType, ParamIdType } from "./user.dto";
import { EmailService } from "./email.service";

@Injectable()
@Controller("/users")
export class UserController {
  constructor(private emailService: EmailService) {}

  @Get("/")
  async list() {
    return { users: ["Alice", "Bob", "Charlie"] };
  }

  @Post({
    path: "/",
    body: CreateUserDTO,
  })
  async create(@Body() body: CreateUserType) {
    // Send welcome email using injected service
    if (body.email && body.name) {
      await this.emailService.sendWelcomeEmail(body.email, body.name);
    }

    return { message: "User created", data: body };
  }

  @Get({ path: "/:id", params: UserIdDto })
  async findOne(@Param("id") id: string) {
    return { user: { id, name: "Alice" } };
  }

  @Patch({
    path: "/:id",
    params: UserIdDto,
  })
  async update(@Param("id") id: string, @Body() body: any) {
    if (id === "nonexistent") {
      throw new NotFoundException("User not found");
    }

    return { message: "User updated", id, data: body };
  }
}

4. Bootstrap Your Application

// index.ts
import { WynkFactory } from "wynkjs";
import { UserController } from "./user.controller";

const app = WynkFactory.create({
  controllers: [UserController],
});

await app.listen(3000);

console.log("πŸš€ Server running on http://localhost:3000");

5. Run Your Server

bun run start
# or with --watch for hot reload
bun run dev

6. Test Your API

# List users
curl http://localhost:3000/users

# Create user (with validation)
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"John","email":"john@example.com","age":25}'

# Get user by ID
curl http://localhost:3000/users/123

# Update user
curl -X PATCH http://localhost:3000/users/123 \
  -H "Content-Type: application/json" \
  -d '{"email":"newemail@example.com"}'

That's it! πŸŽ‰


πŸ“š Core Decorators

HTTP Methods

All HTTP method decorators support both string and object formats:

// Simple string format
@Get(path?: string)
@Post(path?: string)
@Put(path?: string)
@Patch(path?: string)
@Delete(path?: string)
@Options(path?: string)
@Head(path?: string)

// Object format with validation
@Get({ path?: string, params?: Schema, query?: Schema })
@Post({ path?: string, body?: Schema, params?: Schema, query?: Schema })
@Put({ path?: string, body?: Schema, params?: Schema, query?: Schema })
@Patch({ path?: string, body?: Schema, params?: Schema, query?: Schema })
@Delete({ path?: string, params?: Schema, query?: Schema })

Examples:

// Simple string path
@Get("/users")
async findAll() { }

// Object with body validation
@Post({ path: "/users", body: CreateUserDTO })
async create(@Body() body: CreateUserType) { }

// Object with multiple validations
@Post({
  path: "/:id1/:id2",
  body: CreateUserDTO,
  params: MultiParamDto,
  query: UserQueryDto
})
async create(@Body() body, @Param("id1") id1, @Query() query) { }

Parameter Decorators

@Param(key?: string)       // Route parameters (single or all)
@Body()                    // Request body (validated by decorator schema)
@Query(key?: string)       // Query parameters (single or all)
@Headers(key?: string)     // Request headers (single or all)
@Req()                     // Full Elysia request object
@Res()                     // Full Elysia response object

Route Options

@HttpCode(statusCode)      // Set HTTP status code
@Header(name, value)       // Set response header
@Redirect(url, code?)      // Redirect response

Middleware

@Use(...middlewares)       // Apply middleware
@UseGuards(...guards)      // Apply guards
@UseInterceptors(...)      // Apply interceptors
@UsePipes(...pipes)        // Apply pipes
@UseFilters(...filters)    // Apply exception filters

πŸ› οΈ CLI Tools

WynkJS provides powerful CLI tools to speed up your development workflow:

⚑ wynkjs-cli - Code Generator

Generate modules, controllers, services, and DTOs instantly:

# Install globally (recommended)
bun add -g wynkjs-cli

# Or install in project
bun add -D wynkjs-cli

Commands:

# Generate complete CRUD module (controller + service + DTO)
wynkjs-cli generate module product
# or short: wynkjs-cli g m product

# Generate controller only (all HTTP methods)
wynkjs-cli generate controller user
# or short: wynkjs-cli g c user

# Generate service only (all CRUD methods)
wynkjs-cli generate service order
# or short: wynkjs-cli g s order

# Generate DTO only (Create, Update, ID DTOs)
wynkjs-cli generate dto payment
# or short: wynkjs-cli g d payment

What it generates:

wynkjs-cli g m product
# Creates:
# src/modules/product/
#   β”œβ”€β”€ product.controller.ts   # Full CRUD controller
#   β”œβ”€β”€ product.service.ts      # All CRUD methods
#   └── product.dto.ts           # Validation schemas

Auto-imports: Controllers are automatically imported and added to src/index.ts!

Generated Code Example:

// product.controller.ts - Ready to use!
@Injectable()
@Controller("/product")
export class ProductController {
  constructor(private productService: ProductService) {}

  @Get("/")
  async findAll() {
    /* ... */
  }

  @Post({ path: "/", body: CreateProductDTO })
  async create(@Body() body: CreateProductType) {
    /* ... */
  }

  @Get({ path: "/:id", params: ProductIdDto })
  async findOne(@Param("id") id: string) {
    /* ... */
  }

  @Put({ path: "/:id", params: ProductIdDto, body: UpdateProductDTO })
  async update(@Param("id") id: string, @Body() body: UpdateProductType) {
    /* ... */
  }

  @Delete({ path: "/:id", params: ProductIdDto })
  async remove(@Param("id") id: string) {
    /* ... */
  }
}

Full Workflow:

# 1. Create new project
bunx create-wynkjs
cd my-api

# 2. Generate your first module
wynkjs g m product

# 3. Start developing
bun run dev

# 4. Your API is ready!
curl http://localhost:3000/product
# {"data":[]}

Custom Configuration (optional):

Create wynkjs.config.json in your project root:

{
  "srcDir": "src",
  "controllersDir": "src/controllers",
  "servicesDir": "src/services",
  "dtoDir": "src/dto",
  "modulesDir": "src/modules"
}

🎯 Features & Examples

🌐 Built-in CORS Support

WynkJS includes built-in CORS support - no additional packages needed!

import { WynkFactory, CorsOptions } from "wynkjs";

// Simple: Allow all origins (development)
const app = WynkFactory.create({
  controllers: [UserController],
  cors: true,
});

// Advanced: Custom CORS with specific origins (production)
const corsOptions: CorsOptions = {
  origin: ["https://yourdomain.com", "https://app.yourdomain.com"],
  credentials: true,
  methods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
  maxAge: 86400, // 24 hours
};

const app = WynkFactory.create({
  controllers: [UserController],
  cors: corsOptions,
});

// Dynamic origin validation (NestJS-style)
const corsOptions: CorsOptions = {
  origin: (origin: string) => {
    const allowedOrigins = ["https://yourdomain.com"];
    return allowedOrigins.includes(origin);
  },
  credentials: true,
};

See CORS.md for complete documentation.

οΏ½ Plugins & Middleware

WynkJS provides a flexible plugin system to extend your application. Add compression, rate limiting, caching, and more using the app.use() API.

Compression Plugin (Built-in)

Automatically compress HTTP responses with Brotli, Gzip, or Deflate:

import { WynkFactory, compression } from "wynkjs";

const app = WynkFactory.create({
  controllers: [UserController],
});

// Add compression middleware
app.use(
  compression({
    threshold: 1024, // Compress responses > 1KB
    encodings: ["br", "gzip", "deflate"], // Prefer brotli, then gzip
  })
);

await app.listen(3000);

Real-world performance:

  • Brotli: 58KB β†’ 2.9KB (95% reduction) ⚑
  • Gzip: 58KB β†’ 7.7KB (87% reduction)

Features:

  • Smart compression (only compresses if it reduces size)
  • Auto-detects client support
  • Configurable threshold and compression levels
  • No external dependencies

See Plugins README for full documentation and options.

Custom Plugins

Create your own plugins:

export function myPlugin(options = {}) {
  return (app: Elysia) => {
    return app
      .onBeforeHandle(async (context) => {
        // Before request
      })
      .onAfterHandle(async (context) => {
        // After request
        return context.response;
      });
  };
}

// Use it
app.use(myPlugin({ option: "value" }));

Swagger / OpenAPI Documentation

✨ New Feature: WynkJS can use Elysia's Swagger plugin for automatic API documentation!

Since WynkJS is built on top of Elysia, you can use the official @elysiajs/swagger plugin to generate interactive API documentation from your WynkJS decorators.

bun add @elysiajs/swagger
import { WynkFactory } from "wynkjs";
import { swagger } from "@elysiajs/swagger";

const app = WynkFactory.create({
  controllers: [UserController, ProductController],
});

const server = await app.build();

// Add Swagger documentation
server.use(
  swagger({
    documentation: {
      info: {
        title: "My API Documentation",
        version: "1.0.0",
        description: "Auto-generated from WynkJS decorators",
      },
      tags: [
        { name: "users", description: "User management" },
        { name: "products", description: "Product catalog" },
      ],
    },
    path: "/docs",
  })
);

server.listen(3000);
// πŸ“š Visit: http://localhost:3000/docs

What gets auto-documented:

βœ… All HTTP routes (@Get, @Post, etc.)
βœ… Query parameters (from DTOs)
βœ… Request body schemas (from DTOs)
βœ… Path parameters (from DTOs)
βœ… Validation rules (min, max, format)
βœ… JWT Authentication support

With JWT Authentication:

server.use(
  swagger({
    documentation: {
      info: { title: "Secure API", version: "1.0.0" },
      components: {
        securitySchemes: {
          bearerAuth: {
            type: "http",
            scheme: "bearer",
            bearerFormat: "JWT",
          },
        },
      },
      security: [{ bearerAuth: [] }],
    },
    path: "/docs",
  })
);

πŸ“š See Swagger Integration Guide for complete examples and best practices.

οΏ½πŸ”’ Authentication with Guards

import { Controller, Get, Use } from "wynkjs";

// Create a simple JWT guard
const jwtGuard = async (ctx: any, next: Function) => {
  const token = ctx.headers.authorization?.replace("Bearer ", "");

  if (!token) {
    ctx.set.status = 401;
    return { error: "Unauthorized" };
  }

  // Verify token and attach user
  ctx.user = await verifyToken(token);
  return next();
};

@Controller("/protected")
export class ProtectedController {
  @Get("/")
  @Use(jwtGuard)
  async getProtectedData() {
    return { message: "This is protected!" };
  }
}

🎭 Role-Based Access Control

const rolesGuard = (allowedRoles: string[]) => {
  return async (ctx: any, next: Function) => {
    const userRole = ctx.user?.role;

    if (!allowedRoles.includes(userRole)) {
      ctx.set.status = 403;
      return { error: "Forbidden" };
    }

    return next();
  };
};

@Controller("/admin")
@Use(jwtGuard, rolesGuard(["admin"]))
export class AdminController {
  @Get("/users")
  async getAllUsers() {
    return { users: [] };
  }
}

πŸ’‰ Dependency Injection

WynkJS includes powerful dependency injection with zero setup required:

// email.service.ts
import { Injectable } from "wynkjs";

@Injectable()
export class EmailService {
  async sendWelcomeEmail(email: string, userName: string): Promise<void> {
    console.log(`πŸ“§ Sending welcome email to ${email}`);
    // Your email sending logic
  }

  async sendPasswordResetEmail(
    email: string,
    resetToken: string
  ): Promise<void> {
    console.log(`πŸ” Sending password reset to ${email}`);
    // Your reset email logic
  }
}

// user.controller.ts
import { Controller, Post, Body, Injectable } from "wynkjs";
import { EmailService } from "./email.service";

@Injectable()
@Controller("/users")
export class UserController {
  // ✨ EmailService is automatically injected!
  constructor(private emailService: EmailService) {}

  @Post("/")
  async create(@Body() body: { name: string; email: string }) {
    // Use the injected service
    await this.emailService.sendWelcomeEmail(body.email, body.name);
    return { message: "User created and email sent!" };
  }

  @Post("/send-reset-email")
  async sendPasswordReset(@Body() body: { email: string }) {
    await this.emailService.sendPasswordResetEmail(
      body.email,
      "reset-token-123"
    );
    return { message: "Password reset email sent" };
  }
}

Available DI decorators:

  • Capital: Injectable, Inject, Singleton, AutoInjectable, Container

οΏ½ Provider System

WynkJS providers are singleton services that initialize when your app starts. Perfect for database connections, configuration, and external services:

import { Injectable, singleton } from "wynkjs";
import { drizzle } from "drizzle-orm/bun-sqlite";
import { Database } from "bun:sqlite";

@Injectable()
@singleton()
export class DatabaseService {
  public db: any;
  private sqlite: Database;

  // Called automatically when app starts
  async onModuleInit() {
    console.log("πŸ”Œ Connecting to database...");
    this.sqlite = new Database("mydb.sqlite", { create: true });
    this.db = drizzle(this.sqlite);
    console.log("βœ… Database connected");
  }

  getDb() {
    return this.db;
  }
}

// Register provider in factory
const app = WynkFactory.create({
  providers: [DatabaseService], // βœ… Initialized on startup
  controllers: [UserController],
});

// Use in controllers/services
@Injectable()
@Controller("/users")
export class UserController {
  constructor(private dbService: DatabaseService) {}

  @Get("/")
  async findAll() {
    const db = this.dbService.getDb();
    return await db.select().from(userTable);
  }
}

Benefits:

  • βœ… Automatic Initialization: Providers init before routes are registered
  • βœ… Error Handling: App won't start if provider fails to initialize
  • βœ… Tight Coupling: Only registered providers are available
  • βœ… Lifecycle Hooks: onModuleInit() for setup, onModuleDestroy() for cleanup
  • βœ… Type Safety: Full TypeScript support with DI

See docs-wynkjs/PROVIDERS.md for complete guide

οΏ½πŸ—ƒοΈ Database Integration (Drizzle ORM)

import { drizzle } from "drizzle-orm/bun-sqlite";
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { InjectTable, registerTables } from "wynkjs";

// Define your table
const userTable = sqliteTable("users", {
  id: integer("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
});

// Register tables
registerTables({ userTable });

@Injectable()
export class UserService {
  private db = drizzle(process.env.DATABASE_URL);

  constructor(@InjectTable(userTable) private table: typeof userTable) {}

  async findAll() {
    return this.db.select().from(this.table);
  }
}

πŸ“ Request Validation

WynkJS provides automatic request validation with full IntelliSense support, custom error messages, and customizable error formats:

// user.dto.ts
import { DTO, CommonDTO } from "wynkjs";

// ✨ Full IntelliSense when typing DTO.String(), DTO.Number(), etc!
// ✨ Add custom error messages with the `error` property
export const CreateUserDTO = DTO.Strict({
  name: DTO.Optional(
    DTO.String({
      minLength: 2,
      maxLength: 50,
      error: "Name must be between 2 and 50 characters",
    })
  ),
  email: CommonDTO.Email({
    description: "User email address",
    error: "Please provide a valid email address",
  }),
  mobile: DTO.Optional(
    DTO.String({
      pattern: "^[6-9]{1}[0-9]{9}$",
      error: "Invalid mobile number format",
    })
  ),
  age: DTO.Optional(
    DTO.Number({
      minimum: 18,
      error: "Age must be at least 18 years",
    })
  ),
});

export interface CreateUserType {
  name?: string;
  email?: string;
  mobile?: string;
  age?: number;
}

export const UserUpdateDTO = DTO.Strict({
  email: DTO.Optional(
    DTO.String({
      format: "email",
      minLength: 5,
      error: "Email must be valid and at least 5 characters",
    })
  ),
  age: DTO.Optional(
    DTO.Number({
      minimum: 18,
      error: "Age must be at least 18 years",
    })
  ),
});

export interface UserUpdateType {
  email?: string;
  age?: number;
}

// user.controller.ts
import { Controller, Post, Patch, Body, Param } from "wynkjs";
import { CreateUserDTO, UserUpdateDTO } from "./user.dto";
import type { CreateUserType, UserUpdateType } from "./user.dto";

@Controller("/users")
export class UserController {
  @Post({
    path: "/",
    body: CreateUserDTO, // βœ… Automatic validation
  })
  async create(@Body() body: CreateUserType) {
    // Body is validated automatically!
    // Invalid requests get clear error messages
    return { message: "User created", data: body };
  }

  @Patch({
    path: "/:id",
    body: UserUpdateDTO, // βœ… Different schema for updates
  })
  async update(@Param("id") id: string, @Body() body: UserUpdateType) {
    return { message: "User updated", id, data: body };
  }
}

Customize validation error format:

WynkJS provides three built-in formatters for validation errors:

import {
  WynkFactory,
  FormatErrorFormatter, // Object-based { field: ["messages"] }
  SimpleErrorFormatter, // Simple array ["message1", "message2"]
  DetailedErrorFormatter, // Detailed with field info
} from "wynkjs";

const app = WynkFactory.create({
  controllers: [UserController],
  // Choose your validation error format
  validationErrorFormatter: new DetailedErrorFormatter(), // βœ… Recommended
});

Available formatters:

  1. FormatErrorFormatter (Object-based):

    {
      "statusCode": 400,
      "message": "Validation failed",
      "errors": {
        "email": ["Invalid email address"],
        "age": ["Must be at least 18"]
      }
    }
  2. SimpleErrorFormatter (Simple array):

    {
      "statusCode": 400,
      "message": "Validation failed",
      "errors": ["Invalid email address", "Must be at least 18"]
    }
  3. DetailedErrorFormatter (Detailed):

    {
      "statusCode": 400,
      "message": "Validation failed",
      "errors": [
        {
          "field": "email",
          "message": "Invalid email address",
          "value": "invalid-email"
        }
      ]
    }

Note: Formatters are for validation errors only (from DTO validation). For runtime exception handling, use exception filters - see Exception Handling section.

See docs-wynkjs/VALIDATION_FORMATTERS.md for all available error formats

✨ Custom Validation Error Messages

WynkJS supports custom error messages at the DTO level using the error or errorMessage property. This allows you to provide user-friendly error messages instead of the default TypeBox validation messages:

// user.dto.ts
import { DTO, CommonDTO } from "wynkjs";

export const CreateUserDTO = DTO.Strict({
  name: DTO.String({
    minLength: 2,
    maxLength: 50,
    error: "Name must be between 2 and 50 characters", // ✨ Custom message
  }),
  email: CommonDTO.Email({
    error: "Invalid email address", // ✨ Custom message
  }),
  mobile: DTO.String({
    pattern: "^[6-9]{1}[0-9]{9}$",
    error: "Invalid mobile number", // ✨ Custom message
  }),
  age: DTO.Number({
    minimum: 18,
    error: "You must be at least 18 years old", // ✨ Custom message
  }),
});

Example response with invalid data:

curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"A","email":"invalid","mobile":"1234567890","age":15}'

Response:

{
  "statusCode": 400,
  "message": "Validation failed",
  "errors": [
    { "field": "name", "message": "Name must be between 2 and 50 characters" },
    { "field": "email", "message": "Invalid email address" },
    { "field": "mobile", "message": "Invalid mobile number" },
    { "field": "age", "message": "You must be at least 18 years old" }
  ]
}

Without custom messages, you would get generic TypeBox messages:

{
  "errors": [
    {
      "field": "name",
      "message": "Expected string length greater or equal to 2"
    },
    { "field": "email", "message": "Expected string to match email format" },
    {
      "field": "mobile",
      "message": "Expected string to match '^[6-9]{1}[0-9]{9}$'"
    },
    { "field": "age", "message": "Expected number greater or equal to 18" }
  ]
}

Note: You can use either error or errorMessage property - both work the same way.

🚫 Exception Handling

WynkJS provides a powerful exception handling system with formatters for validation errors and filters for runtime exceptions.

Exception Classes

Throw HTTP exceptions anywhere in your code:

import { Controller, Get, Param, NotFoundException } from "wynkjs";

@Controller("/users")
export class UserController {
  @Get("/:id")
  async findOne(@Param("id") id: string) {
    // Built-in exceptions
    if (id === "nonexistent") {
      throw new NotFoundException("User not found");
    }

    return { user: { id, name: "Alice" } };
  }
}

Built-in exceptions:

  • BadRequestException - 400
  • UnauthorizedException - 401
  • ForbiddenException - 403
  • NotFoundException - 404
  • ConflictException - 409
  • InternalServerErrorException - 500
  • And many more...

Validation Error Formatting

Format validation errors using formatters (passed to factory options):

import { WynkFactory, DetailedErrorFormatter } from "wynkjs";

const app = WynkFactory.create({
  controllers: [UserController],
  validationErrorFormatter: new DetailedErrorFormatter(), // βœ… For validation
});

Available formatters:

  • FormatErrorFormatter - Object format { field: ["messages"] }
  • SimpleErrorFormatter - Simple array ["message1", "message2"]
  • DetailedErrorFormatter - Detailed with field info

Global Exception Filters

Handle runtime exceptions using global filters:

import {
  WynkFactory,
  DatabaseExceptionFilter,
  NotFoundExceptionFilter,
  GlobalExceptionFilter,
} from "wynkjs";

const app = WynkFactory.create({
  controllers: [UserController],
  validationErrorFormatter: new DetailedErrorFormatter(),
});

// Register global exception filters
app.useGlobalFilters(
  new DatabaseExceptionFilter(), // Handles database errors
  new NotFoundExceptionFilter(), // Smart 404 handling with response data checking
  new GlobalExceptionFilter() // Catch-all for other exceptions
);

Available global filters:

  • DatabaseExceptionFilter - Catches database errors (unique constraints, foreign keys, etc.)
  • NotFoundExceptionFilter - Smart filter that only formats truly empty 404 responses
  • FileUploadExceptionFilter - Handles file upload errors
  • GlobalExceptionFilter - Catch-all for unhandled exceptions

What's the difference?

Feature Formatters Filters
Purpose Format validation errors Handle runtime exceptions
When? During request validation (TypeBox) When exceptions are thrown
Registration WynkFactory.create({ validationErrorFormatter }) app.useGlobalFilters()
Example FormatErrorFormatter DatabaseExceptionFilter

Custom Exception Filters

Create your own filters for specific routes:

import { WynkExceptionFilter, ExecutionContext, Catch } from "wynkjs";

@Catch() // Catches all exceptions
export class CustomExceptionFilter implements WynkExceptionFilter {
  catch(exception: any, context: ExecutionContext) {
    const request = context.getRequest();

    return {
      statusCode: exception.statusCode || 500,
      message: exception.message,
      timestamp: new Date().toISOString(),
      path: request.url,
    };
  }
}

// Use globally
app.useGlobalFilters(new CustomExceptionFilter());

// Or on specific controllers/routes
@UseFilters(CustomExceptionFilter)
@Controller("/api")
export class ApiController {}

See ARCHITECTURE.md for complete architecture details

πŸ”„ Multiple Params and Query Validation

// user.dto.ts
export const MultiParamDto = DTO.Object({
  id1: DTO.String({ minLength: 2, maxLength: 50 }),
  id2: DTO.String({ minLength: 2, maxLength: 50 }),
});

export const UserQueryDto = DTO.Strict({
  includePosts: DTO.Optional(DTO.Boolean({ default: false })),
  includeComments: DTO.Optional(DTO.Boolean({ default: false })),
});

// user.controller.ts
@Post({
  path: "/:id1/:id2",
  body: CreateUserDTO,
  params: MultiParamDto,  // βœ… Validates route params
  query: UserQueryDto,     // βœ… Validates query params
})
async create(
  @Body() body: CreateUserType,
  @Param("id1") id1: string,
  @Param("id2") id2: string,
  @Query() query: UserQueryType
) {
  return {
    message: "User created",
    data: body,
    params: { id1, id2 },
    query
  };
}

πŸ”„ Multiple Middleware

const logger = async (ctx: any, next: Function) => {
  console.log(`${ctx.request.method} ${ctx.path}`);
  return next();
};

const cors = async (ctx: any, next: Function) => {
  ctx.set.headers["Access-Control-Allow-Origin"] = "*";
  return next();
};

@Controller("/api")
@Use(logger, cors)
export class ApiController {
  @Get("/data")
  @Use(cacheMiddleware)
  async getData() {
    return { data: [] };
  }
}

πŸ—οΈ Project Structure

Recommended project structure for WynkJS applications:

my-wynk-app/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ modules/
β”‚   β”‚   β”œβ”€β”€ user/
β”‚   β”‚   β”‚   β”œβ”€β”€ user.controller.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ user.service.ts
β”‚   β”‚   β”‚   └── user.dto.ts
β”‚   β”‚   └── product/
β”‚   β”‚       β”œβ”€β”€ product.controller.ts
β”‚   β”‚       β”œβ”€β”€ product.service.ts
β”‚   β”‚       └── product.dto.ts
β”‚   β”œβ”€β”€ exceptions/
β”‚   β”‚   └── custom.exceptions.ts
β”‚   β”œβ”€β”€ guards/
β”‚   β”‚   └── auth.guard.ts
β”‚   β”œβ”€β”€ filters/
β”‚   β”‚   └── http-exception.filter.ts
β”‚   └── index.ts
β”œβ”€β”€ package.json
└── tsconfig.json

Module-based Organization:

  • Each feature/domain lives in its own module folder
  • Controllers, services, and DTOs are co-located
  • Easy to navigate and maintain
  • Generated automatically by wynkjs-cli

πŸ“– API Reference

Core Decorators

@Controller(basePath?: string)

Define a controller class with optional base path.

@Controller("/users")
export class UserController {
  // All routes will be prefixed with /users
}

HTTP Method Decorators

All HTTP methods support both string and object formats:

String format:

@Get("/")                              // GET /users
@Post("/")                             // POST /users
@Patch("/:id")                         // PATCH /users/:id

Object format with validation:

@Get({ path: "/" })                    // GET with options

@Post({
  path: "/",
  body: CreateUserDTO                  // POST with body validation
})

@Patch({
  path: "/:id",
  params: UserIdDto,                   // PATCH with param validation
  body: UpdateUserDTO                  // PATCH with body validation
})

@Get({
  path: "/",
  query: UserQueryDto                  // GET with query validation
})

@Post({
  path: "/:id1/:id2",
  body: CreateUserDTO,                 // Multiple validations
  params: MultiParamDto,
  query: UserQueryDto
})

Available HTTP methods:

  • @Get() - GET requests (params, query)
  • @Post() - POST requests (body, params, query)
  • @Put() - PUT requests (body, params, query)
  • @Patch() - PATCH requests (body, params, query)
  • @Delete() - DELETE requests (params, query)
  • @Options() - OPTIONS requests
  • @Head() - HEAD requests

@Body() / @Param(key?) / @Query(key?)

Parameter extraction decorators.

async create(
  @Body() body: CreateUserType,      // Full body
  @Param("id") id: string,            // Single param
  @Query() query: UserQueryType       // Full query object
) {}

@Injectable()

Mark a class as injectable for dependency injection.

@Injectable()
export class EmailService {
  // This service can be injected into controllers
}

DTO Helpers

DTO.Strict(properties, options?)

Create object schema that strips additional properties.

const UserDTO = DTO.Strict({
  name: DTO.String(),
  email: DTO.String({ format: "email" }),
});

CommonDTO Patterns

Pre-built validation patterns for common use cases.

CommonDTO.Email(); // Email validation
CommonDTO.Name(); // Name (2-50 chars)
CommonDTO.Password(); // Password (min 6 chars)
CommonDTO.UUID(); // UUID format
CommonDTO.PhoneIN(); // Indian phone number

Exception Classes

throw new NotFoundException("User not found"); // 404
throw new BadRequestException("Invalid input"); // 400
throw new UnauthorizedException("Not authenticated"); // 401
throw new ForbiddenException("Access denied"); // 403
throw new InternalServerErrorException("Error"); // 500

πŸ”— Resources


🀝 Contributing

We welcome contributions from the community! Whether you're fixing bugs, improving documentation, or proposing new features, your help is appreciated.

πŸ› Reporting Issues

If you find a bug or have a feature request:

  1. Check existing issues to avoid duplicates
  2. Create a new issue with a clear title and description
  3. Provide details: Steps to reproduce, expected behavior, actual behavior
  4. Include environment info: Bun version, OS, WynkJS version

Report an issue β†’

πŸ’‘ Contributing Code

Getting Started

  1. Fork the repository

    # Fork on GitHub, then clone your fork
    git clone https://github.com/YOUR_USERNAME/wynkjs-core.git
    cd wynkjs-core
  2. Install dependencies

    bun install
  3. Create a branch

    git checkout -b feature/your-feature-name
    # or
    git checkout -b fix/bug-description
  4. Make your changes in the appropriate package:

    • core/ - Core framework decorators and utilities
    • packages/create-wynkjs/ - Project scaffolding CLI
    • packages/wynkjs-cli/ - Code generator CLI
  5. Test your changes

    # Test in the example project
    cd example
    bun run dev
    
    # Test CLI generation
    cd /tmp && bunx /path/to/wynkjs-core/packages/create-wynkjs
  6. Build all packages

    # From project root
    bun run build
    cd packages/create-wynkjs && bun run build
    cd ../wynkjs-cli && bun run build
  7. Commit your changes

    git add .
    git commit -m "feat: add new feature"
    # or
    git commit -m "fix: resolve issue with decorators"

    Commit Convention:

    • feat: - New feature
    • fix: - Bug fix
    • docs: - Documentation changes
    • refactor: - Code refactoring
    • test: - Adding tests
    • chore: - Maintenance tasks
  8. Push and create a Pull Request

    git push origin feature/your-feature-name

    Then open a Pull Request on GitHub with:

    • Clear description of changes
    • Link to related issues
    • Screenshots/examples if applicable

Code Style

  • TypeScript: Strict mode enabled
  • Formatting: Use Prettier (run bun run format if available)
  • Linting: Follow ESLint rules
  • Naming:
    • PascalCase for classes and interfaces
    • camelCase for functions and variables
    • kebab-case for file names

Testing Guidelines

  • Test your changes in the example/ directory
  • Ensure existing examples still work
  • Add new examples for new features
  • Test CLI tools in a fresh directory

πŸ“ Documentation

Documentation improvements are always welcome!

  • README updates: Keep examples current and clear
  • Code comments: Add JSDoc comments for public APIs
  • Guides: Create helpful guides in docs-wynkjs/
  • Examples: Add real-world usage examples

πŸ’¬ Community

  • GitHub Discussions: Ask questions and share ideas
  • Discord: (Coming soon) Join our community chat
  • Twitter: Follow @wynkjs for updates

πŸ“œ License

By contributing, you agree that your contributions will be licensed under the MIT License.


Thank you for contributing to WynkJS! πŸŽ‰


About

this is wynkjs core module

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages