An unopinionated microframework built with Express, TypeScript, Zod, Swagger
npm install codeweaverExpress and TypeScript. Its modular architecture for routers promotes scalability and organized development, making it easy to expand and maintain. Routers are automatically discovered and wired up through a conventional folder structure, simplifying project organization and reducing boilerplate. Routers can be nested, allowing you to compose complex route trees by placing sub-routers inside parent router folders.
bash
npm i -g codeweaver
npx codeweaver my-app
cd my-app
`
2. Clone the repository:
Using pnpm:
`bash
pnpm i
`
Using npm:
`bash
npm i
`
3. Run the application:
Using pnpm:
`bash
pnpm start
`
Using npm:
`bash
npm start
`
4. Visit the Swagger UI: Open your browser and go to http://localhost:3000/api-docs to view the automatically generated API documentation.
5. Build: Compile the TypeScript files for the production environment:
Using pnpm:
`bash
pnpm run build
pnpm run serve
`
Using npm:
`bash
npm run build
npm run serve
`
Sample Project Structure
/src
├── /routers Directory containing all router files
│ ├── /users Routers for user-related endpoints
│ │ ├── index.router.ts /users
│ │ ├── user-router2.router.ts /users/user-router2
│ │ ├── user.controller.ts
│ │ ├── user.service.ts
│ │ └── user.dto.ts
│ ├── /products Routers for product-related endpoints
│ │ ├── index.router.ts /products
│ │ ├── product.controller.ts
│ │ ├── product.service.ts
│ │ ├── /dtos
│ │ │ └── product.dto.ts
| | │ └── product-types.dto.ts
│ ├── /orders Routers for order-related endpoints
│ │ ├── index.router.ts /orders
│ │ ├── order.controller.ts
│ │ ├── order.controller.spec.ts
│ │ ├── order.service.ts
│ │ └── order.service.spec.ts
│ └── index.router.ts Home page
│ └── app.controller.ts Home page
├── app.ts Main application file
├── config.ts Application configuration file
└── ... Other files (middleware, models, etc.)
$3
Each router file in the /routers directory is organized to handle related endpoints. The app.ts file automatically imports all routers and mounts them on the main Express application, making it straightforward to add new routes without touching central code.
Files ending with .router.ts or .router.js are automatically included in the router list and can be reused for various purposes within the application.
Example of a basic router:
`typescript
import { Router, Request, Response } from "express";
import asyncHandler from "express-async-handler";
import UserController from "./user.controller";
import { resolve } from "@/core/container";
const router = Router();
const userController = resolve(UserController);
/**
* @swagger
* /users:
* post:
* summary: Create a user
* description: Create a new user.
* consumes:
* - application/json
* produces:
* - application/json
* parameters:
* - in: body
* name: user
* required: true
* schema:
* type: object
* required:
* - username
* - email
* - password
* properties:
* username:
* type: string
* minLength: 3
* example: JessicaSmith
* email:
* type: string
* format: email
* example: user@example.com
* password:
* type: string
* minLength: 6
* example: securePassword123
* responses:
* 201:
* description: User created
*/
router.post(
"/",
asyncHandler(async (req: Request, res: Response) => {
const user = await userController.validateUserCreationDto(req.body);
await userController.create(user);
res.status(201).send();
})
);
/**
* @swagger
* /users/{id}:
* get:
* summary: Get a user by ID
* parameters:
* - name: id
* in: path
* required: true
* description: The ID of the product
* schema:
* type: integer
* responses:
* 200:
* description: A user object
* 404:
* description: user not found
*/
router.get(
"/:id",
asyncHandler(async (req: Request, res: Response) => {
const id = await userController.validateId(req.params.id);
const user = await userController.get(id);
res.json(user);
})
);
/**
* @swagger
* /users:
* get:
* summary: Get users
* description: Returns a list of user objects.
* responses:
* 200:
* description: A list of user objects
*/
router.get(
"/",
asyncHandler(async (req: Request, res: Response) => {
res.json(await userController.getAll());
})
);
export = router;
`
$3
Controllers in this Express TypeScript framework act as the intermediary between the incoming HTTP requests and the application logic. Each controller is responsible for handling specific routes and defining the behavior associated with those routes. This organization promotes a clean architecture by separating business logic, validation, and routing concerns.
Controllers can be organized within the router folders, allowing them to stay closely related to their respective routes. However, they are not limited to this structure and can be placed anywhere within the src folder as needed, providing flexibility in organizing the codebase.
Controllers leverage decorators from the utils-decorators package to implement throttling, caching, and error handling gracefully.
For example, in the provided UserController, the createUser method demonstrates how to apply error handling through decorators. It also employs @rateLimit to restrict the number of allowed requests within a specified timeframe, effectively guarding against too many rapid submissions. When an error arises, the @onError decorator provides a systematic way to handle exceptions, allowing for logging or other error management processes to be performed centrally.
Here’s a brief breakdown of key components used in the UserController:
`typescript
import { UserCreationDto, UserDto, ZodUserDto } from "./dto/user.dto";
import { ResponseError } from "@/core/error";
import { convert, stringToInteger } from "@/core/helpers";
import { config } from "@/config";
import { users } from "@/db";
import { User, ZodUser } from "@/entities/user.entity";
import { Invalidate, MapAsyncCache, Memoize } from "@/core/cache";
import { Injectable } from "@/core/container";
import { parallelMap } from "@/core/parallel";
import { ErrorHandler, LogMethod, Timeout } from "@/core/middlewares";
import { RateLimit } from "@/core/rate-limit";
async function invalidInputHandler(e: ResponseError) {
const message = "Invalid input";
throw new ResponseError(message, 400, e?.message);
}
const userCache = new MapAsyncCache(config.cacheSize);
const usersCache = new MapAsyncCache(1);
@Injectable()
/**
* Controller for handling user-related operations
* @class UserController
* @desc Provides methods for user management including CRUD operations
*/
export default class UserController {
// constructor(private readonly userService: UserService) { }
@ErrorHandler(invalidInputHandler)
/**
* Validates a string ID and converts it to a number.
*
* @param {string} id - The ID to validate and convert.
* @returns {number} The numeric value of the provided ID.
*/
public async validateId(id: string): Promise {
return stringToInteger(id);
}
@ErrorHandler(invalidInputHandler)
/**
* Validates and creates a new User from the given DTO.
*
* @param {UserCreationDto} user - The incoming UserCreationDto to validate and transform.
* @returns {User} A fully formed User object ready for persistence.
*/
public async validateUserCreationDto(user: UserCreationDto): Promise {
return await convert(user, ZodUser);
}
@Invalidate(usersCache, true)
@RateLimit(config.rateLimitTimeSpan, config.rateLimitAllowedCalls)
/**
* Create a new user
* @param {User} user - User creation data validated by Zod schema
* @returns {Promise}
* @throws {ResponseError} 500 - When rate limit exceeded
* @throws {ResponseError} 400 - Invalid input data
*/
public async create(user: User): Promise {
users.push(user);
}
@Memoize(usersCache, () => "key")
@Timeout(config.timeout)
@RateLimit(config.rateLimitTimeSpan, config.rateLimitAllowedCalls)
/**
* Get all users
* @returns {Promise} List of users with hidden password fields
* @throws {ResponseError} 500 - When rate limit exceeded
*/
public async getAll(
timeoutSignal?: AbortSignal
): Promise<(UserDto | null)[]> {
return await parallelMap(users, async (user) =>
timeoutSignal?.aborted == false
? await convert(user!, ZodUserDto)
: null
);
}
@Memoize(userCache)
@RateLimit(config.rateLimitTimeSpan, config.rateLimitAllowedCalls)
/**
* Get user by ID
* @param {number} id - User ID as string
* @returns {Promise} User details or error object
* @throws {ResponseError} 404 - User not fou
* @throws {ResponseError} 400 - Invalid ID format
*/
public async get(id: number): Promise {
const user = users.find((user) => user.id === id);
if (user == null) {
throw new ResponseError("User not found");
}
return convert(user, ZodUserDto);
}
}
``