@alevnyacow/nzmt 0.15.19 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +31 -38
  2. package/bin/cli.js +34 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,34 +4,18 @@
4
4
  ![NPM License](https://img.shields.io/npm/l/%40alevnyacow%2Fnzmt)
5
5
  ![npm bundle size (scoped)](https://img.shields.io/bundlephobia/minzip/%40alevnyacow/nzmt)
6
6
 
7
- # What
7
+ # TL;DR 🕰
8
8
 
9
- Next Zod Modules Toolkit. Next.js tools you actually missed + a scaffolder for server logic & client queries. **Not a framework.** Full-stack, batteries included.
9
+ You can scaffold safe runtime-validated production-ready **server modules** with DDD-inspired structure and **ready-to-use React Queries** in one CLI command. Fully wired, editable, and modules are **easily usable as Server Actions** — no boilerplate.
10
10
 
11
- Build full-stack features in Next.js without boilerplate. ⚡
11
+ # What and Why
12
12
 
13
- # TL;DR
13
+ Next Zod Modules Toolkit. Next.js tools you actually missed + a scaffolder for server logic & client queries. **Not a framework.** Full-stack, batteries included to build full-stack features in Next.js without boilerplate. ⚡
14
14
 
15
- One command:
16
-
17
- `npx nzmt crud-api user`
18
-
19
- Gives you:
20
-
21
- - entity and stores (Prisma and in-memory)
22
- - fully typed API routes
23
- - services (for Server Actions)
24
- - Zod validation
25
- - React Query hooks
26
-
27
- All wired together and fully editable. No boilerplate. See `Quick start with Prisma` for a full working example.
28
-
29
- # Why
30
-
31
- - ☕ Keep using plain Next.js — just faster and cleaner. Skip the moment when some “helpful” framework fights you, making you wonder if coding it yourself would’ve been easier.
15
+ - ☕ Keep using plain Next.js — just faster and cleaner.
32
16
  - 🧙 Focus on your domain logic without drowning in full-blown DDD.
33
- - ✨ DI, Zod validation, project structure, handy API controllers and Server actions out of the box.
34
- - 🪄 Services, controllers, client queries, and other programmer stuff appear at the snap of a finger — and yes, it’s fun. (Well, not *literally* at the snap of a finger — that’s just marketing, to be honest. You still need to run one CLI command.)
17
+ - ✨ DI, handy API controllers and a bunch of other cool things out of the box aimed at improving your DX.
18
+ - 🪄 Services, controllers, client queries, and other programmer stuff appear at the snap of a finger. (Well, not *literally* at the snap of a finger — that’s just marketing, to be honest. You still need to run one CLI command.)
35
19
 
36
20
  # Quick start with Prisma
37
21
 
@@ -106,26 +90,25 @@ And after one CLI command and few tweaks you can use your React query hooks or S
106
90
  Schema: Client → React Query → API → Controller → Service → Store → DB
107
91
  ```
108
92
 
109
- Everything is already scaffolded for you, just import it and use! ✨
93
+ Everything is already scaffolded and grouped in handy namespace for you, just import it and use! Even invalidations are working out of the box (though you can modify scaffolded queries any way you want)!
110
94
 
111
95
  ```tsx
112
96
  'use client'
113
97
 
114
- import { useUserAPI_GET } from "@/client/shared/queries/user-controller/GET";
115
- import { useUserAPI_POST } from "@/client/shared/queries/user-controller/POST";
98
+ import { UserQueries } from "@/client/shared/queries/user";
116
99
 
117
100
  export default function Home() {
118
- const { mutate: addUser } = useUserAPI_POST()
119
- const { data, isFetching } = useUserAPI_GET({ query: {} })
101
+ const { mutate: addUser } = UserQueries.usePOST()
102
+ const { data, isFetching } = UserQueries.useGET({ query: {} })
120
103
 
121
- const addGreg = () => {
122
- addUser({ body: { payload: { name: 'Greg' } } })
104
+ const addColinZeal = () => {
105
+ addUser({ body: { payload: { name: 'Colin Zeal' } } })
123
106
  }
124
107
 
125
108
  return (
126
109
  <div>
127
- <button onClick={addGreg}>
128
- Add Greg
110
+ <button onClick={addColinZeal}>
111
+ Add Mr. Zeal
129
112
  </button>
130
113
 
131
114
  {isFetching ? 'Loading users...' : JSON.stringify(data)}
@@ -135,7 +118,7 @@ export default function Home() {
135
118
 
136
119
  ```
137
120
 
138
- ### How to use server actions
121
+ ### How to use server modules as server actions
139
122
 
140
123
  ```
141
124
  Schema: Server Action → Service → Store → DB
@@ -150,6 +133,11 @@ import { fromDI } from "@/server/di"
150
133
  import type { UserService } from "@/server/services/user"
151
134
 
152
135
  export default async function() {
136
+ /**
137
+ * FYI: `fromDI` argument is strongly typed and
138
+ * this type automatically updates after you scaffold
139
+ * anything. Cool, right?
140
+ */
153
141
  const userService = fromDI<UserService>('UserService')
154
142
 
155
143
  const driver8 = await userService.getDetails({
@@ -165,6 +153,14 @@ export default async function() {
165
153
 
166
154
  # Common questions
167
155
 
156
+ ## Can I tweak scaffolded files?
157
+
158
+ Yes — everything is fully editable, including configuration. Think of NZMT as a shadcn-style approach for full-stack: scaffold first, then fully own the code.
159
+
160
+ If you need to tweak something, NZMT won’t get in your way. Your changes are preserved on subsequent generations. For example, if you modify a generated query and regenerate later, your edits stay intact.
161
+
162
+ NZMT is designed for a plug-and-play experience — everything works out of the box. At the same time, it’s just a set of helpers to turn Zod schemas into service, store, and controller contracts, with a powerful scaffolder. No magic here — all code is yours to modify.
163
+
168
164
  ## Do I really need to understand DI and other fancy concepts to use NZMT?
169
165
 
170
166
  No. NZMT provides you safe and intuitive facade above `inversifyjs` and automatically registers dependencies. To get an instance you just use `fromDI` function with strongly typed keys in any place of your server code like this:
@@ -173,10 +169,6 @@ No. NZMT provides you safe and intuitive facade above `inversifyjs` and automati
173
169
  const userService = fromDI<UserService>('UserService')
174
170
  ```
175
171
 
176
- ## Can I tweak scaffolded files?
177
-
178
- Yes — everything is fully editable, including configuration. You can think of NZMT as shadcn-style approach for server-side logic — scaffold, then fully own the code.
179
-
180
172
  ## Why data layer modules are called `Stores` and not `Repositories`?
181
173
 
182
174
  Good design is impossible without precise terminology. The definition of a "Repository" can vary depending on the terminology used. It’s frustrating when you’ve spent your whole life writing repositories, and then some smart aleck comes along and accuses you of having been writing, say, Data Access Objects all this time! In general, a "Repository" is simply a pattern for working with data. Often, what we really need isn’t a specific pattern, but a properly separated abstraction layer for data handling, which we can then adapt to our needs. That’s exactly why the names of the modules used for the Data Layer in NZMT are kept as abstract as possible, without tying them to any specific data-handling pattern.
@@ -200,7 +192,7 @@ P.S. In general, you remain within plain Next.js.
200
192
 
201
193
  ## Why not use Nest or tRPC?
202
194
 
203
- Again, you can use whatever you want, God bless you.
195
+ Still you can use whatever you want, God bless you.
204
196
 
205
197
  `NZMT` sits between `tRPC` and `NestJS`:
206
198
 
@@ -211,6 +203,7 @@ But:
211
203
  - no framework lock-in
212
204
  - no magic runtime
213
205
  - full control over your code
206
+ - no new layers of client-server interaction
214
207
 
215
208
  Just better Next.js.
216
209
 
package/bin/cli.js CHANGED
@@ -1080,11 +1080,13 @@ function generateQueries(lowerCase, upperCase) {
1080
1080
  return acc
1081
1081
  }, {})
1082
1082
 
1083
- const controllerQueriesPath = path.resolve(projectRoot, `${config.coreFolder}${config.paths.queries}`, `${entityName}-api`, 'queries')
1084
-
1083
+ const controllerQueriesPath = path.resolve(projectRoot, `${config.coreFolder}${config.paths.queries}`, `${entityName}`, 'queries')
1084
+ let scaffoldedMethods = []
1085
+
1085
1086
  fs.mkdirSync(controllerQueriesPath, { recursive: true })
1086
1087
 
1087
1088
  for (const rootMethod of rootMethods) {
1089
+ scaffoldedMethods.push(rootMethod)
1088
1090
  if (!rootMethod) {
1089
1091
  continue
1090
1092
  }
@@ -1094,7 +1096,7 @@ function generateQueries(lowerCase, upperCase) {
1094
1096
  continue
1095
1097
  }
1096
1098
  fs.writeFileSync(fileName, [
1097
- `import { ${rootMethod === 'GET' ? 'useQuery' : 'useMutation'} } from '@tanstack/react-query'`,
1099
+ `import { ${rootMethod === 'GET' ? 'useQuery' : 'useMutation, useQueryClient'} } from '@tanstack/react-query'`,
1098
1100
  `import type { ${upperCase}API } from '@${config.paths.controllers}/${entityName}'`,
1099
1101
  `import { apiRequest } from '@${config.paths.clientUtils}'`,
1100
1102
  '',
@@ -1106,15 +1108,17 @@ function generateQueries(lowerCase, upperCase) {
1106
1108
  ? [
1107
1109
  `export const use${rootMethod} = (payload: Method['payload']) => {`,
1108
1110
  `\treturn useQuery<Method['response'], Method['error']>({`,
1109
- `\t\tqueryKey: [endpoint, payload],`,
1111
+ `\t\tqueryKey: ['${entityName}', '${rootMethod}', payload],`,
1110
1112
  `\t\tqueryFn: () => apiRequest(endpoint, 'GET')(payload)`,
1111
1113
  `\t})`,
1112
1114
  `}`
1113
1115
  ].join('\n')
1114
1116
  : [
1115
1117
  `export const use${rootMethod} = () => {`,
1118
+ `\tconst queryClient = useQueryClient()`,
1116
1119
  `\treturn useMutation<Method['response'], Method['error'], Method['payload']>({`,
1117
- `\t\tmutationFn: apiRequest(endpoint, '${rootMethod}')`,
1120
+ `\t\tmutationFn: apiRequest(endpoint, '${rootMethod}'),`,
1121
+ `\t\tonSuccess: () => { queryClient.invalidateQueries({ queryKey: ['${entityName}'], exact: false }) }`,
1118
1122
  `\t})`,
1119
1123
  `}`
1120
1124
  ].join('\n')
@@ -1124,13 +1128,17 @@ function generateQueries(lowerCase, upperCase) {
1124
1128
  for (const [currentPath, methods] of Object.entries(nestedMethods)) {
1125
1129
  for (const method of methods) {
1126
1130
  const fullMethodName = `${currentPath.replaceAll('/', '_')}_${method}`
1131
+ scaffoldedMethods.push(fullMethodName)
1127
1132
  const fileName = path.resolve(controllerQueriesPath, `${fullMethodName}.ts`)
1128
1133
  const alreadyExists = fs.existsSync(fileName)
1129
1134
  if (alreadyExists) {
1130
1135
  continue
1131
1136
  }
1137
+
1138
+ const nameForHook = (fullMethodName.charAt(0).toUpperCase() + fullMethodName.slice(1)).replaceAll('_', '');
1139
+
1132
1140
  fs.writeFileSync(fileName, [
1133
- `import { ${method === 'GET' ? 'useQuery' : 'useMutation'} } from '@tanstack/react-query'`,
1141
+ `import { ${method === 'GET' ? 'useQuery' : 'useMutation, useQueryClient' } } from '@tanstack/react-query'`,
1134
1142
  `import type { ${upperCase}API } from '@${config.paths.controllers}/${entityName}'`,
1135
1143
  `import { apiRequest } from '@${config.paths.clientUtils}'`,
1136
1144
  '',
@@ -1140,24 +1148,39 @@ function generateQueries(lowerCase, upperCase) {
1140
1148
  ``,
1141
1149
  method === 'GET'
1142
1150
  ? [
1143
- `export const use${upperCase}API_${fullMethodName} = (payload: Method['payload']) => {`,
1151
+ `export const use${nameForHook} = (payload: Method['payload']) => {`,
1144
1152
  `\treturn useQuery<Method['response'], Method['error']>({`,
1145
- `\t\tqueryKey: [endpoint, payload],`,
1153
+ `\t\tqueryKey: ['${entityName}', ${currentPath.split('/').map(x => `'${x}'`).join(', ')}, payload],`,
1146
1154
  `\t\tqueryFn: () => apiRequest(endpoint, 'GET')(payload)`,
1147
1155
  `\t})`,
1148
1156
  `}`
1149
1157
  ].join('\n')
1150
1158
  : [
1151
- `export const use${upperCase}API_${fullMethodName} = () => {`,
1159
+ `export const use${nameForHook} = () => {`,
1160
+ `\tconst queryClient = useQueryClient()`,
1152
1161
  `\treturn useMutation<Method['response'], Method['error'], Method['payload']>({`,
1153
- `\t\tmutationFn: apiRequest(endpoint, '${method}')`,
1162
+ `\t\tmutationFn: apiRequest(endpoint, '${method}'),`,
1163
+ `\t\tonSuccess: () => { queryClient.invalidateQueries({ queryKey: ['${entityName}'], exact: false }) }`,
1154
1164
  `\t})`,
1155
1165
  `}`
1156
1166
  ].join('\n')
1157
1167
  ].join('\n'))
1158
1168
 
1159
1169
  }
1160
- }
1170
+ }
1171
+
1172
+ const allQueryFiles = fs.readdirSync(controllerQueriesPath, { withFileTypes: true }).filter(x => x.isFile())
1173
+ const deprecatedQueries = allQueryFiles.filter(x => scaffoldedMethods.every(scaffolded => !x.name.startsWith(scaffolded)))
1174
+
1175
+ for (const deprecated of deprecatedQueries) {
1176
+ fs.rmSync(path.resolve(controllerQueriesPath, deprecated))
1177
+ }
1178
+
1179
+ fs.writeFileSync(path.resolve(controllerQueriesPath, 'index.ts'), scaffoldedMethods.map(x => `export * from './${x}'`).join('\n'))
1180
+
1181
+ const indexPath = path.resolve(projectRoot, `${config.coreFolder}${config.paths.queries}`, `${entityName}`)
1182
+ fs.writeFileSync(path.resolve(indexPath, 'index.ts'), `export * as ${upperCase}Queries from './queries'`)
1183
+
1161
1184
  }
1162
1185
 
1163
1186
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alevnyacow/nzmt",
3
- "version": "0.15.19",
3
+ "version": "0.16.1",
4
4
  "description": "Next Zod Modules Toolkit",
5
5
  "repository": {
6
6
  "type": "git",