A high-performance TypeScript first framework built for Bun with Elegant Decorator-Based Architecture
10x faster than Express/NestJs, built for modern TypeScript development for Bun β‘
Quick Start β’ CLI Tools β’ Features β’ Documentation
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.
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.)
Quickly scaffold a new WynkJS project with best practices:
# Create a new project
bunx create-wynkjs
# or
npx create-wynkjsWhat 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 --watchfor 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 reloadbun run start- Production serverbun run build- Build TypeScriptbun run lint- Run ESLintbun 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// 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;
}// 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
}
}// 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 };
}
}// 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");bun run start
# or with --watch for hot reload
bun run dev# 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! π
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) { }@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@HttpCode(statusCode) // Set HTTP status code
@Header(name, value) // Set response header
@Redirect(url, code?) // Redirect response@Use(...middlewares) // Apply middleware
@UseGuards(...guards) // Apply guards
@UseInterceptors(...) // Apply interceptors
@UsePipes(...pipes) // Apply pipes
@UseFilters(...filters) // Apply exception filtersWynkJS provides powerful CLI tools to speed up your development workflow:
Generate modules, controllers, services, and DTOs instantly:
# Install globally (recommended)
bun add -g wynkjs-cli
# Or install in project
bun add -D wynkjs-cliCommands:
# 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 paymentWhat 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 schemasAuto-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"
}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.
WynkJS provides a flexible plugin system to extend your application. Add compression, rate limiting, caching, and more using the app.use() API.
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.
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" }));β¨ 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/swaggerimport { 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/docsWhat 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.
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!" };
}
}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: [] };
}
}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
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
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);
}
}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:
-
FormatErrorFormatter (Object-based):
{ "statusCode": 400, "message": "Validation failed", "errors": { "email": ["Invalid email address"], "age": ["Must be at least 18"] } } -
SimpleErrorFormatter (Simple array):
{ "statusCode": 400, "message": "Validation failed", "errors": ["Invalid email address", "Must be at least 18"] } -
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
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.
WynkJS provides a powerful exception handling system with formatters for validation errors and filters for runtime exceptions.
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- 400UnauthorizedException- 401ForbiddenException- 403NotFoundException- 404ConflictException- 409InternalServerErrorException- 500- And many more...
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
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 responsesFileUploadExceptionFilter- Handles file upload errorsGlobalExceptionFilter- 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 |
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
// 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
};
}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: [] };
}
}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
Define a controller class with optional base path.
@Controller("/users")
export class UserController {
// All routes will be prefixed with /users
}All HTTP methods support both string and object formats:
String format:
@Get("/") // GET /users
@Post("/") // POST /users
@Patch("/:id") // PATCH /users/:idObject 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
Parameter extraction decorators.
async create(
@Body() body: CreateUserType, // Full body
@Param("id") id: string, // Single param
@Query() query: UserQueryType // Full query object
) {}Mark a class as injectable for dependency injection.
@Injectable()
export class EmailService {
// This service can be injected into controllers
}Create object schema that strips additional properties.
const UserDTO = DTO.Strict({
name: DTO.String(),
email: DTO.String({ format: "email" }),
});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 numberthrow 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- π Full Documentation
- π‘ Example Code
- ποΈ Architecture Guide - Complete guide to formatters vs filters
- π§ Provider System - NEW! Database, config, and service providers
- π Migration Guide - Upgrading from older versions
- π CLI Tool (create-wynkjs)
- π¨ Validation Formatters
- π Changelog
- π Report Issues
We welcome contributions from the community! Whether you're fixing bugs, improving documentation, or proposing new features, your help is appreciated.
If you find a bug or have a feature request:
- Check existing issues to avoid duplicates
- Create a new issue with a clear title and description
- Provide details: Steps to reproduce, expected behavior, actual behavior
- Include environment info: Bun version, OS, WynkJS version
-
Fork the repository
# Fork on GitHub, then clone your fork git clone https://github.com/YOUR_USERNAME/wynkjs-core.git cd wynkjs-core
-
Install dependencies
bun install
-
Create a branch
git checkout -b feature/your-feature-name # or git checkout -b fix/bug-description -
Make your changes in the appropriate package:
core/- Core framework decorators and utilitiespackages/create-wynkjs/- Project scaffolding CLIpackages/wynkjs-cli/- Code generator CLI
-
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
-
Build all packages
# From project root bun run build cd packages/create-wynkjs && bun run build cd ../wynkjs-cli && bun run build
-
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 featurefix:- Bug fixdocs:- Documentation changesrefactor:- Code refactoringtest:- Adding testschore:- Maintenance tasks
-
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
- TypeScript: Strict mode enabled
- Formatting: Use Prettier (run
bun run formatif available) - Linting: Follow ESLint rules
- Naming:
- PascalCase for classes and interfaces
- camelCase for functions and variables
- kebab-case for file names
- 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 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
- GitHub Discussions: Ask questions and share ideas
- Discord: (Coming soon) Join our community chat
- Twitter: Follow @wynkjs for updates
By contributing, you agree that your contributions will be licensed under the MIT License.
Thank you for contributing to WynkJS! π