A self-assembled, opinionated Node.js API framework combining Koa, dependency injection, Zod validation, and auto-generated OpenAPI documentation.
| Library | Role |
|---|---|
| Koa v3 | HTTP web framework |
| @koa/router | Route registration |
| @koa/cors | CORS middleware |
| koa-bodyparser | Request body parsing |
| awilix | IoC / Dependency injection container |
| zod | Schema validation (request body, query, params, env config) |
| zod-openapi | Auto-generate OpenAPI 3.1 spec from Zod schemas |
| TypeORM | ORM for PostgreSQL |
| typeorm-naming-strategies | Auto snake_case column naming |
| dotenv | Environment variable loading |
| ramda | Functional utilities |
| axios | HTTP client |
| moment | Date/time utilities |
| TypeScript | Language |
| ts-node + nodemon | Development hot-reload server |
| tsup | Production bundler |
| jest + ts-jest | Unit testing |
Node.js version: v22 (see .nvmrc)
src/
├── index.ts # Entry point: bootstraps Koa app, registers middleware & routes
├── setupContainer.ts # Root DI wiring: calls repo & service container setup
│
├── config/
│ └── index.ts # Loads .env and validates required vars with Zod
│
├── router/ # Route handlers (auto-scanned at startup)
│ ├── user.ts # User CRUD routes + Zod schemas for OpenAPI
│ └── openapi.ts # GET /openapi — serves the generated OpenAPI JSON
│
├── service/ # Business logic layer
│ ├── setupContainer.ts # Registers services into the DI container
│ └── user.ts # UserService (calls repository)
│
├── repository/ # Data access layer
│ ├── setupContainer.ts # Registers repositories into the DI container
│ └── user.ts # UserRepo (DB queries / in-memory mock)
│
├── middleware/
│ └── validator.ts # routerSchemaCheck — Zod middleware for body/query/params
│
├── api-response/ # Standardized HTTP response helpers
│ ├── base.ts # BaseResponse (status + code)
│ ├── general.ts # GeneralResponse<T> — 200 success wrapper
│ ├── error.ts # ErrorResponse / BadRequestErrorResponse
│ └── utils.ts # generalResponse() / createErrorResponse() for Koa ctx
│
├── lib/
│ ├── container.ts # Creates the global awilix container instance
│ └── openapi.ts # register() + createOpenApiDocument() — builds the OAS spec
│
├── type/
│ ├── api.ts # Shared types: SchemaCheck, Options
│ └── openapi.ts # OpenApiSchema interface
│
└── typeorm/
├── datasource.ts # PostgreSQL DataSource config (supports IP & Unix socket)
├── entity/ # TypeORM entity definitions
├── migration/ # Database migrations
└── type/ # Custom decorators & shared TypeORM types
All services and repositories are registered into a single awilix container at startup using CLASSIC injection mode (constructor parameter order).
setupContainer()
├── setupRepoContainer() → registers UserRepo
└── setupServiceContainer() → registers UserService (receives UserRepo via DI)
To add a new service: create the class, register it in the relevant setupContainer.ts, then resolve it in the router via container.resolve<YourService>('yourService').
Each router file exports a schemas array of OpenApiSchema objects alongside the route handlers. At startup, index.ts auto-scans the src/router/ directory, calls register(schemas) for each file, and builds the OpenAPI document on demand at GET /openapi.
Wrap any route with routerSchemaCheck(schema) to validate body, query, and params through Zod. On failure it returns a structured 400 error response automatically.
Create a .env file in the project root:
PORT=6969
APP_NAME=elegant-api-framework
HOST=0.0.0.0
# Required only if using TypeORM / PostgreSQL
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=app_db
POSTGRES_USER=app_user
POSTGRES_PASSWORD=secret
DB_CONNECT_MODE=IP # IP | UNIX_SOCKETnvm use # use Node.js version from .nvmrc
npm install
npm run dev # nodemon + ts-node, hot-reload on file changesnpm run build # tsc → dist/
npm run start # node dist/src/index.jsnpm run bundle # tsup → dist/index.js# Generate a new migration from entity changes
npm run mg:gen -- src/typeorm/migration/MigrationName
# Run pending migrations
npm run mg:run
# Revert the last migration
npm run mg:revert
# Create an empty migration file
npm run mg:create -- src/typeorm/migration/MigrationNamenpm test# Create a user
curl -X POST http://localhost:6969/user \
-H "Content-Type: application/json" \
-d '{"name": "Aaron H"}'
# Get all users
curl http://localhost:6969/user
# Get user by ID
curl http://localhost:6969/user/1
# Get OpenAPI spec (paste into https://editor.swagger.io/)
curl http://localhost:6969/openapi