@alevnyacow/nzmt 0.15.19 → 0.16.0
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.
- package/README.md +31 -38
- package/bin/cli.js +34 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,34 +4,18 @@
|
|
|
4
4
|

|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# TL;DR 🕰
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
11
|
+
# What and Why
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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,
|
|
34
|
-
- 🪄 Services, controllers, client queries, and other programmer stuff appear at the snap of a finger
|
|
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 {
|
|
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 } =
|
|
119
|
-
const { data, isFetching } =
|
|
101
|
+
const { mutate: addUser } = UserQueries.usePOST()
|
|
102
|
+
const { data, isFetching } = UserQueries.useGET({ query: {} })
|
|
120
103
|
|
|
121
|
-
const
|
|
122
|
-
addUser({ body: { payload: { name: '
|
|
104
|
+
const addColinZeal = () => {
|
|
105
|
+
addUser({ body: { payload: { name: 'Colin Zeal' } } })
|
|
123
106
|
}
|
|
124
107
|
|
|
125
108
|
return (
|
|
126
109
|
<div>
|
|
127
|
-
<button onClick={
|
|
128
|
-
Add
|
|
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
|
-
|
|
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}
|
|
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: [
|
|
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${
|
|
1151
|
+
`export const use${nameForHook} = (payload: Method['payload']) => {`,
|
|
1144
1152
|
`\treturn useQuery<Method['response'], Method['error']>({`,
|
|
1145
|
-
`\t\tqueryKey: [
|
|
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${
|
|
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
|
|