{"_id":"@alis-kit/template-be","_rev":"2-ceae4af204c114123e5fae1c73478819","name":"@alis-kit/template-be","dist-tags":{"latest":"0.3.0"},"versions":{"0.1.0":{"name":"@alis-kit/template-be","version":"0.1.0","keywords":["fastify","mongoose","rest-api","scaffold","template","backend"],"author":{"name":"alisdev"},"license":"ISC","_id":"@alis-kit/template-be@0.1.0","maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"bin":{"alis-kit-template":"bin/create.mjs"},"dist":{"shasum":"b8a9493abaca8cfd0f6a3dffec881cf6ed57b01e","tarball":"https://registry.npmjs.org/@alis-kit/template-be/-/template-be-0.1.0.tgz","fileCount":18,"integrity":"sha512-k/qmvkubTeSi5YknfZfmWIWPWARdZ2/CkV7zvB0AjrZhLP+YLzv+XJjF5ef27nj3puumGcPb+hG/7LonHCelYw==","signatures":[{"sig":"MEQCIHZ6lNBfRv1stCHwtAxT5DCZqbO+jaxHgdt9e4Y9JAbsAiB4yNV18+95EKG/9tlBNOrAjgXpnMX/f7yShpT1RAG9Mg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":20632},"main":"dist/App.js","type":"module","_from":"file:alis-kit-template-be-0.1.0.tgz","engines":{"bun":">=1.0","node":">=20"},"scripts":{"dev":"nodemon","lint":"eslint \"src/**/*.ts\"","test":"vitest run","build":"tsc -p tsconfig.json","start":"node --import ./dist/polyfill.js dist/App.js","format":"prettier --write \"src/**/*.ts\"","dev:bun":"bun --watch src/App.ts","lint:fix":"eslint \"src/**/*.ts\" --fix","build:bun":"bun build src/App.ts --outdir dist --target bun","start:bun":"bun src/App.ts","typecheck":"tsc -p tsconfig.json --noEmit","test:watch":"vitest","format:check":"prettier --check \"src/**/*.ts\""},"_npmUser":{"name":"alisdev","email":"ibnu.ali56@gmail.com"},"_resolved":"C:\\Users\\IBNU~1.BAD\\AppData\\Local\\Temp\\523ac02339521dcdb4152d426885658a\\alis-kit-template-be-0.1.0.tgz","_integrity":"sha512-k/qmvkubTeSi5YknfZfmWIWPWARdZ2/CkV7zvB0AjrZhLP+YLzv+XJjF5ef27nj3puumGcPb+hG/7LonHCelYw==","_npmVersion":"11.6.2","description":"Base Template Rest API — Scaffold REST API with Fastify + Mongoose + Zod","directories":{},"_nodeVersion":"24.12.0","dependencies":{"zod":"^4.4.3","dotenv":"^17.4.2","fastify":"^5.10.0","mongoose":"^9.8.0","pino-pretty":"^13.1.3","jsonwebtoken":"^9.0.3","@fastify/cors":"^11.3.0","dotenv-expand":"^13.0.0","@fastify/cookie":"^11.1.2","@alis-kit/routers":"^3.0.0","@alis-kit/mongoose":"^1.2.0","@fastify/multipart":"^10.1.0"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^10.8.0","vitest":"^4.1.10","prettier":"^3.9.6","@eslint/js":"^10.0.1","typescript":"~5.9.3","@types/node":"^26.1.1","typescript-eslint":"^8.65.0","@types/jsonwebtoken":"^9.0.10"},"optionalDependencies":{"tsx":"^4.23.1","nodemon":"^3.1.14"},"_npmOperationalInternal":{"tmp":"tmp/template-be_0.1.0_1785317752317_0.2538324907070122","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@alis-kit/template-be","version":"0.3.0","description":"Base Template Rest API — Scaffold REST API with Fastify + Mongoose + Zod","type":"module","main":"dist/app.js","bin":{"alis-kit-template":"bin/create.mjs"},"engines":{"node":">=20","bun":">=1.0"},"keywords":["fastify","mongoose","rest-api","scaffold","template","backend"],"author":{"name":"alisdev"},"license":"ISC","dependencies":{"@alis-kit/mongoose":"^2.0.0","@alis-kit/routers":"^3.0.0","@fastify/cookie":"^11.1.2","@fastify/cors":"^11.3.0","@fastify/multipart":"^10.1.0","dotenv":"^17.4.2","dotenv-expand":"^13.0.0","fastify":"^5.10.0","jsonwebtoken":"^9.0.3","mongoose":"^9.8.0","pino-pretty":"^13.1.3","zod":"^4.4.3"},"devDependencies":{"@eslint/js":"^10.0.1","@types/jsonwebtoken":"^9.0.10","@types/node":"^26.1.1","eslint":"^10.8.0","nodemon":"^3.1.14","prettier":"^3.9.6","tsc-alias":"^1.9.1","tsx":"^4.23.1","typescript":"~5.9.3","typescript-eslint":"^8.65.0","vitest":"^4.1.10"},"scripts":{"dev":"nodemon","dev:bun":"bun --watch src/app.ts","build":"tsc -p tsconfig.json && tsc-alias -p tsconfig.json","build:bun":"bun build src/app.ts --outdir dist --target bun","start":"node --import ./dist/polyfill.js dist/app.js","start:bun":"bun src/app.ts","typecheck":"tsc -p tsconfig.json --noEmit","lint":"eslint \"src/**/*.ts\"","lint:fix":"eslint \"src/**/*.ts\" --fix","format":"prettier --write \"src/**/*.ts\"","format:check":"prettier --check \"src/**/*.ts\"","test":"vitest run","test:watch":"vitest"},"_id":"@alis-kit/template-be@0.3.0","_integrity":"sha512-gNwd2HIhkY+vgxzfPKUNiXuFBhSvRYhnhmFQouHykO5onXrV9yMNm6bU1uBz9TKiISZRO5ZmtUfg7bEPazGJhA==","_resolved":"C:\\Users\\IBNU~1.BAD\\AppData\\Local\\Temp\\8c4fbd761ba11fbc200db0f89f5ae28a\\alis-kit-template-be-0.3.0.tgz","_from":"file:alis-kit-template-be-0.3.0.tgz","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-gNwd2HIhkY+vgxzfPKUNiXuFBhSvRYhnhmFQouHykO5onXrV9yMNm6bU1uBz9TKiISZRO5ZmtUfg7bEPazGJhA==","shasum":"4bf3c8eadca4ecf0cd0c8c33e4e9d9a189f7fe7e","tarball":"https://registry.npmjs.org/@alis-kit/template-be/-/template-be-0.3.0.tgz","fileCount":38,"unpackedSize":68090,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIG6aA+V+dYSjVOoSdV6kMTyAiq9GYoaT5INs2UygZP8/AiEAns/dL6BromkLs4RXIx04XZQV8Yra9Uq93jnd+m+RS3o="}]},"_npmUser":{"name":"alisdev","email":"ibnu.ali56@gmail.com"},"directories":{},"maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/template-be_0.3.0_1786611092750_0.8677334500438745"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-29T09:35:52.118Z","modified":"2026-08-13T08:51:33.115Z","0.1.0":"2026-07-29T09:35:52.472Z","0.3.0":"2026-08-13T08:51:32.881Z"},"author":{"name":"alisdev"},"license":"ISC","keywords":["fastify","mongoose","rest-api","scaffold","template","backend"],"description":"Base Template Rest API — Scaffold REST API with Fastify + Mongoose + Zod","maintainers":[{"name":"alisdev","email":"ibnu.ali56@gmail.com"}],"readme":"# alis-kit-template-be\n\nBase template REST API — **Fastify** + **Mongoose** + **Zod** + **JWT**, dibangun di atas [`@alis-kit/mongoose`](https://www.npmjs.com/package/@alis-kit/mongoose) dan [`@alis-kit/routers`](https://www.npmjs.com/package/@alis-kit/routers).\n\n[![npm version](https://img.shields.io/npm/v/@alis-kit/template-be.svg)](https://www.npmjs.com/package/@alis-kit/template-be)\n[![license](https://img.shields.io/npm/l/@alis-kit/template-be.svg)](#license)\n\n---\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [Manual Setup](#manual-setup)\n- [Scripts](#scripts)\n- [Tech Stack](#tech-stack)\n- [Project Structure](#project-structure)\n- [Environment Variables](#environment-variables)\n- [Defining Entities (`@alis-kit/mongoose`)](#defining-entities-alis-kitmongoose)\n- [Database Layer — What's Included](#database-layer--whats-included)\n- [Building Controllers (`@alis-kit/routers`)](#building-controllers-alis-kitrouters)\n- [Putting It Together — Full Example](#putting-it-together--full-example)\n- [Authentication (JWT)](#authentication-jwt)\n- [License](#license)\n\n---\n\n## Quick Start\n\n```bash\nnpx @alis-kit/template-be\ncd my-project\nnpm run dev\n```\n\nWith Bun:\n\n```bash\nbunx @alis-kit/template-be\ncd my-project\nbun run dev:bun\n```\n\n## Manual Setup\n\n```bash\ngit clone <repo> my-project\ncd my-project\nnpm install\ncp .env.example .env\n# edit .env with your config\nnpm run dev\n```\n\n---\n\n## Scripts\n\n| npm | bun | Description |\n|-----|-----|-------------|\n| `npm run dev` | `bun run dev:bun` | Development with watch |\n| `npm run build` | `bun run build:bun` | Build for production |\n| `npm start` | `bun run start:bun` | Start production server |\n| `npm run typecheck` | `bun run typecheck` | TypeScript check |\n| `npm run lint` | `bun run lint` | Lint source |\n| `npm run format` | `bun run format` | Format source with Prettier |\n| `npm test` | `bun run test` | Run the test suite (Vitest) |\n\n---\n\n## Tech Stack\n\n- **Runtime**: Node.js >=20 / Bun >=1.0\n- **Framework**: Fastify v5\n- **ODM**: [`@alis-kit/mongoose`](https://www.npmjs.com/package/@alis-kit/mongoose) (Mongoose v9 wrapper — decorator-based schema & repository)\n- **Validation**: Zod v4\n- **Decorators / Routing**: [`@alis-kit/routers`](https://www.npmjs.com/package/@alis-kit/routers) v3\n\n---\n\n## Project Structure\n\nLayers are grouped by type, then by domain. Files are named `<domain>.<layer>.ts`.\n\n```\nmy-project/\n├── src/\n│   ├── schemas/            # @Schema-decorated entities (one folder per domain)\n│   │   ├── index.ts        # Barrel — imported by app.ts so entities get registered\n│   │   └── user/           # user, user.profile, user.token\n│   ├── repositories/       # @Repository data access + *.query.ts (pure query builders)\n│   │   └── user/\n│   ├── models/\n│   │   ├── types/<domain>/ # Zod: request payloads (IUser) & documents (IUserSchema)\n│   │   ├── enums/<domain>/ # Domain enums (role, gender, ...)\n│   │   └── filter/         # Zod search filters per collection\n│   ├── controllers/        # @RestController-decorated route handlers\n│   ├── config/             # env.ts, database.ts, server.ts\n│   ├── utils/              # Small cross-domain helpers\n│   ├── __tests__/          # *.spec.ts\n│   ├── app.ts              # Bootstrap: register entities → connect DB → serve\n│   └── polyfill.ts         # Symbol.metadata polyfill for decorators\n├── .env.example\n├── package.json\n└── tsconfig.json\n```\n\nTwo conventions worth knowing before you add code:\n\n- **Imports use the `@/` alias and keep the `.js` extension** (NodeNext), e.g.\n  `import User from \"@/schemas/user/user.schema.js\"`. `tsc-alias` resolves it on\n  build; `vitest.config.ts` maps it for tests.\n- **A `@Schema` class only registers when its file is imported.** Add every new\n  entity to `src/schemas/index.ts`, which `app.ts` imports on startup.\n\n---\n\n## Environment Variables\n\n```bash\n# .env.example\n# ─── Runtime ─────────────────────────────────────────────────────────\nNODE_ENV=development\nPORT=5000\nHOST=0.0.0.0\nLOG_LEVEL=info\n\n# ─── CORS ────────────────────────────────────────────────────────────\n# Comma-separated allowlist untuk production. Dev boleh \"*\".\nCORS_ORIGIN=*\n....\n```\nlook in file `.env.example`\n\n---\n\n## Defining Entities (`@alis-kit/mongoose`)\n\nEntities are defined with `@Schema`, validated at runtime with Zod, and exposed through a `@Repository` class. See the [`@alis-kit/mongoose` docs](https://www.npmjs.com/package/@alis-kit/mongoose) for the full API (relations, TTL, soft delete, indexes).\n\n> **Field names are stored verbatim.** The generated Mongoose schema is\n> `strict: false` and only registers the `BaseEntity` fields, so a property named\n> `userId` is stored as `userId` — there is no camelCase → snake_case mapping.\n> `@Index`, `@Relation({ foreignField })`, and `SearchCustom.of()` must all use\n> the exact same spelling, otherwise you get an index on a field that does not\n> exist and relations that always resolve to `null`.\n\n```typescript\n// src/schemas/user/user.schema.ts — the entity (collection definition)\nimport { BaseEntity, Index, Relation, Schema } from \"@alis-kit/mongoose\";\nimport { type IUserProfileSchema } from \"@/models/types/user/user.profile.type.js\";\n\n@Index({ email: 1 }, { unique: true })\n@Schema({ collection: \"users\", timestamps: true, idStrategy: \"uuid\" })\nexport default class User extends BaseEntity {\n  email!: string;\n  password!: string;\n  isActive!: boolean;\n\n  // Inverse one-to-one: the profile holds `userId`, so the join runs\n  // users._id → user_profiles.userId.\n  @Relation({ collection: \"user_profiles\", localField: \"_id\", foreignField: \"userId\" })\n  profile!: IUserProfileSchema | null;\n}\n```\n\n```typescript\n// src/models/types/user/user.type.ts — Zod shape of the stored document\nimport { z } from \"zod\";\nimport { BaseEntitySchema } from \"@alis-kit/mongoose\";\n\nexport const IUserSchema = BaseEntitySchema.extend({\n  email: z.string(),\n  password: z.string(),\n  isActive: z.boolean(),\n});\nexport type IUserSchema = z.infer<typeof IUserSchema>;\n```\n\n```typescript\n// src/repositories/user/user.repository.ts — data access\nimport { BaseRepository, Repository } from \"@alis-kit/mongoose\";\nimport User from \"@/schemas/user/user.schema.js\";\nimport { type IUserSchema } from \"@/models/types/user/user.type.js\";\n\n@Repository(User)\nexport default class UserRepository extends BaseRepository<IUserSchema> {}\n```\n\n---\n\n## Database Layer — What's Included\n\nThe template ships with a working `user` domain you can copy for your own\ndomains. It covers the data layer only — services, routes, and password hashing\nare intentionally left to you.\n\n| Collection | Entity | Holds |\n|---|---|---|\n| `users` | `schemas/user/user.schema.ts` | Login credentials (`email`, `password` hash, `isActive`) |\n| `user_profiles` | `schemas/user/user.profile.schema.ts` | Personal data, one document per user |\n| `user_tokens` | `schemas/user/user.token.schema.ts` | One document per active refresh-token session |\n\n```typescript\nconst users = new UserRepository();\n\n// Paginated list. `profile` is joined automatically (eager relation);\n// the password hash is never included.\nconst page = await users.search({ email: \"budi\", isActive: true }, Pageable.of(1, 10));\n\n// The only read that carries the password hash — for the login flow.\nconst account = await users.findByEmailForAuth(\"budi@example.com\");\n\n// Searching by name goes through the profile repository (see note below).\nconst profiles = new UserProfileRepository();\nawait profiles.search({ keyword: \"budi\", role: UserRole.ADMIN }, Pageable.of(1, 10));\n\n// Sessions: issue on login, revoke on logout, revoke all on password change.\nconst sessions = new UserTokenRepository();\nawait sessions.issue(user._id, rawRefreshToken, expiresAt);\nawait sessions.revokeAllByUser(user._id);\n```\n\nConventions this layer follows — worth keeping when you add a domain:\n\n- **Query building lives in `*.query.ts` as pure functions**, separate from the\n  repository. They are testable without a database, and filters that are\n  `undefined` are skipped — passing them through would produce\n  `{ field: undefined }` and silently match nothing.\n- **Search by a child collection's fields runs on that collection.** The kit's\n  `OPERATION_JOIN_*` operations only join forward (a local field holding the\n  target's `_id`), so an inverse relation like user → profile cannot be filtered\n  from the `users` side. Name search therefore lives in `UserProfileRepository`,\n  where the fields are local and indexed.\n- **Secrets stay out of reads.** Every `UserRepository` read projects\n  `USER_PUBLIC_FIELDS`; refresh tokens are stored as a SHA-256 hash, access\n  tokens are not stored at all, and the session relation is `lazy` so it never\n  rides along with a user read.\n- **Expiry and deletion are built in.** `expireAt` (from `BaseEntity`) has a TTL\n  index, so expired sessions clean themselves up; logout is a soft delete.\n\n> Entity files can't be imported from tests — Vitest's bundled esbuild does not\n> transform TC39 decorators yet. Test the `*.query.ts` modules, filters, and\n> utils; verify decorator wiring against `dist/` after `npm run build`.\n\n---\n\n## Building Controllers (`@alis-kit/routers`)\n\nControllers use TC39 native decorators to define routes, request validation, and Swagger metadata. See the [`@alis-kit/routers` docs](https://www.npmjs.com/package/@alis-kit/routers) for the full API (auth, cookies, exceptions, response envelopes).\n\n```typescript\n// src/controllers/user.controller.ts\nimport { z } from \"zod\";\nimport {\n  RestController, Authentication, Tag, Description,\n  GetMapping, PostMapping,\n  ReqBody, ReqQuery, ReqParam,\n  Response, HttpStatus,\n} from \"@alis-kit/routers\";\nimport { Pageable } from \"@alis-kit/mongoose\";\nimport UserRepository from \"@/repositories/user/user.repository.js\";\n\nconst CreateUserSchema = z.object({\n  firstName: z.string().min(1),\n  lastName: z.string().min(1),\n  email: z.email(), // Zod v4 — not z.string().email()\n  password: z.string().min(8),\n});\n\nconst PaginationSchema = z.object({\n  page: z.coerce.number().default(1),\n  size: z.coerce.number().default(10),\n});\n\n@RestController(\"/users\")\n@Authentication(\"auth\")\n@Tag(\"User Management\")\n@Description(\"User management CRUD\")\nexport class UserController {\n  private userRepo = new UserRepository();\n\n  @GetMapping(\"/\")\n  @Description(\"List users with pagination\")\n  @ReqQuery(PaginationSchema)\n  @Response(200, \"List of users\")\n  async getAll(query: z.infer<typeof PaginationSchema>) {\n    return this.userRepo.findAll(undefined, Pageable.of(query.page, query.size));\n  }\n\n  @GetMapping(\"/:id\")\n  @Description(\"Get user detail by ID\")\n  @ReqParam(\"id\")\n  @Response(200, \"User detail\")\n  @Response(404, \"User not found\")\n  async getById(id: string) {\n    return this.userRepo.findById(id);\n  }\n\n  @PostMapping(\"/\")\n  @HttpStatus(201)\n  @ReqBody(CreateUserSchema)\n  @Response(201, \"User created successfully\")\n  async create(body: z.infer<typeof CreateUserSchema>) {\n    return this.userRepo.save(body, { actorId: \"system\" });\n  }\n}\n```\n\n---\n\n## Putting It Together — Full Example\n\nThe template already wires this up across three files, so adding a domain means\ntouching two of them:\n\n```typescript\n// src/app.ts — registers entities, connects to MongoDB, then serves\nimport { env } from \"@/config/env.js\";\nimport Server from \"@/config/server.js\";\nimport { ConnectDatabase, DisconnectDatabase } from \"@/config/database.js\";\nimport \"@/schemas/index.js\"; // ← every entity must be imported to be registered\n\nasync function bootstrap(): Promise<void> {\n  await ConnectDatabase();\n  const app = await Server();\n  await app.listen({ host: env.HOST, port: env.PORT });\n  // ... graceful shutdown on SIGTERM/SIGINT\n}\n\nvoid bootstrap();\n```\n\n```typescript\n// src/config/server.ts — Fastify, plugins, error handler, route registration\nRouterKit.setup({\n  framework: \"fastify\",\n  app,\n  responseEnvelope: \"raw\",\n  globalPrefix: \"/api\",\n});\n\nRouterKit.register(HealthController /*, UserController, ... */);\nRouterKit.handleNotFound();\n```\n\nSo a new domain is: add the entity to `src/schemas/index.ts`, and add the\ncontroller to `RouterKit.register(...)`.\n\n---\n\n## Authentication (JWT)\n\nRegister an auth middleware with `RouterKit.setup()` so routes marked with `@Authentication(\"auth\")` are protected automatically:\n\n```typescript\nimport jwt from \"jsonwebtoken\";\n\nRouterKit.setup({\n  framework: \"fastify\",\n  app,\n  authMiddleware: async (req) => {\n    const token = req.headers.authorization?.replace(\"Bearer \", \"\");\n    if (!token) throw new Error(\"Missing token\");\n    return jwt.verify(token, env.JWT_ACCESS_SECRET);\n  },\n});\n```\n\nIssue tokens on login (e.g. inside an `AuthController`), and record the refresh\ntoken as a session so it can be revoked later:\n\n```typescript\nimport jwt from \"jsonwebtoken\";\nimport { env } from \"@/config/env.js\";\nimport UserTokenRepository from \"@/repositories/user/user.token.repository.js\";\n\nconst accessToken = jwt.sign({ sub: user._id, email: user.email }, env.JWT_ACCESS_SECRET, {\n  expiresIn: env.JWT_ACCESS_TTL,\n});\n\nconst refreshToken = jwt.sign({ sub: user._id }, env.JWT_REFRESH_SECRET, {\n  expiresIn: env.JWT_REFRESH_TTL,\n});\n\n// Stored as a SHA-256 hash; the raw token only ever lives in the httpOnly cookie.\nawait new UserTokenRepository().issue(user._id, refreshToken, expiresAt);\n```\n\n> Password hashing is **not** included — hash with argon2 or bcrypt in your\n> service layer before calling `save()`. `utils/hash.util.ts` is for refresh\n> tokens only, never for passwords.\n\n---\n\n## License\n\nMIT","readmeFilename":"README.md"}