@atozapi/nextjs 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +87 -2
- package/index.js +316 -0
- package/package.json +35 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AtoZAPI Maintainers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,88 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @atozapi/nextjs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> FastAPI-style request validation and OpenAPI metadata wrapper for Next.js App Router route handlers.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@atozapi/nextjs)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ⚡ Overview
|
|
11
|
+
|
|
12
|
+
`@atozapi/nextjs` brings the developer experience of **FastAPI** to **Next.js App Router** (`app/api/**/route.js|ts`):
|
|
13
|
+
|
|
14
|
+
- 🛡️ **Declarative Schema Validation**: Enforce required fields, string constraints, numbers, and regex patterns.
|
|
15
|
+
- 🚫 **FastAPI-Standard 422 Errors**: Standardized RFC-compliant error payloads (`detail: [{ loc, msg, type }]`).
|
|
16
|
+
- ⚡ **Native Web Request Support**: Compatible with native Web standard `Request` and `Response` streams.
|
|
17
|
+
- 📖 **Zero-Code OpenAPI Extraction**: Exposes route metadata for automatic scanning by `@atozapi/cli`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @atozapi/nextjs
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🚀 Quick Start
|
|
30
|
+
|
|
31
|
+
In your Next.js route handler (e.g. `app/api/users/route.js`):
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
import { NextResponse } from 'next/server';
|
|
35
|
+
import { nextAtozapi } from '@atozapi/nextjs';
|
|
36
|
+
|
|
37
|
+
const UserCreateSchema = {
|
|
38
|
+
username: { type: 'string', minLength: 3, maxLength: 30 },
|
|
39
|
+
email: { type: 'string', pattern: '^[^@]+@[^@]+\\.[^@]+$' },
|
|
40
|
+
age: { type: 'integer', minimum: 18, optional: true }
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const POST = nextAtozapi(UserCreateSchema, async (req, { validatedBody }) => {
|
|
44
|
+
// validatedBody is strictly typed, validated, and sanitized
|
|
45
|
+
return NextResponse.json({
|
|
46
|
+
status: 'success',
|
|
47
|
+
user: validatedBody
|
|
48
|
+
}, { status: 201 });
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Automatic 422 Error Response
|
|
53
|
+
|
|
54
|
+
When invalid or missing data is sent, `@atozapi/nextjs` immediately responds with HTTP 422:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"detail": [
|
|
59
|
+
{
|
|
60
|
+
"loc": ["body", "username"],
|
|
61
|
+
"msg": "String should have at least 3 characters",
|
|
62
|
+
"type": "string_too_short",
|
|
63
|
+
"ctx": { "min_length": 3 }
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"loc": ["body", "email"],
|
|
67
|
+
"msg": "Field required",
|
|
68
|
+
"type": "missing"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 📚 OpenAPI Documentation
|
|
77
|
+
|
|
78
|
+
Use the `@atozapi/cli` scanner to automatically inspect your Next.js routes and serve interactive Swagger UI docs:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx @atozapi/cli serve
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 📄 License
|
|
87
|
+
|
|
88
|
+
MIT © [AtoZAPI Maintainers](https://github.com/dhirajparihar/AtoZAPI)
|
package/index.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @atozapi/nextjs
|
|
3
|
+
* FastAPI-style request validation and OpenAPI metadata wrapper for Next.js App Router route handlers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates a value against a field schema definition.
|
|
8
|
+
* @param {string[]} loc - Location array for FastAPI error representation
|
|
9
|
+
* @param {any} value - Value to validate
|
|
10
|
+
* @param {object|string} rule - Field validation rule
|
|
11
|
+
* @param {Array} errors - Error collection array
|
|
12
|
+
* @returns {any}
|
|
13
|
+
*/
|
|
14
|
+
function validateField(loc, value, rule, errors) {
|
|
15
|
+
const config = typeof rule === 'string' ? { type: rule, required: true } : (rule || {});
|
|
16
|
+
const isRequired = config.required !== false && !config.optional;
|
|
17
|
+
|
|
18
|
+
if (value === undefined || value === null || value === '') {
|
|
19
|
+
if (isRequired) {
|
|
20
|
+
errors.push({
|
|
21
|
+
loc: [...loc],
|
|
22
|
+
msg: 'Field required',
|
|
23
|
+
type: 'missing'
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return config.default !== undefined ? config.default : value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const expectedType = (config.type || 'string').toLowerCase();
|
|
30
|
+
|
|
31
|
+
switch (expectedType) {
|
|
32
|
+
case 'string': {
|
|
33
|
+
if (typeof value !== 'string') {
|
|
34
|
+
errors.push({
|
|
35
|
+
loc: [...loc],
|
|
36
|
+
msg: 'Input should be a valid string',
|
|
37
|
+
type: 'string_type'
|
|
38
|
+
});
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
if (typeof config.minLength === 'number' && value.length < config.minLength) {
|
|
42
|
+
errors.push({
|
|
43
|
+
loc: [...loc],
|
|
44
|
+
msg: `String should have at least ${config.minLength} characters`,
|
|
45
|
+
type: 'string_too_short',
|
|
46
|
+
ctx: { min_length: config.minLength }
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (typeof config.maxLength === 'number' && value.length > config.maxLength) {
|
|
50
|
+
errors.push({
|
|
51
|
+
loc: [...loc],
|
|
52
|
+
msg: `String should have at most ${config.maxLength} characters`,
|
|
53
|
+
type: 'string_too_long',
|
|
54
|
+
ctx: { max_length: config.maxLength }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (config.pattern) {
|
|
58
|
+
const regex = config.pattern instanceof RegExp ? config.pattern : new RegExp(config.pattern);
|
|
59
|
+
if (!regex.test(value)) {
|
|
60
|
+
errors.push({
|
|
61
|
+
loc: [...loc],
|
|
62
|
+
msg: `String should match pattern '${regex.source}'`,
|
|
63
|
+
type: 'string_pattern_mismatch'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (config.format === 'email') {
|
|
68
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
69
|
+
if (!emailRegex.test(value)) {
|
|
70
|
+
errors.push({
|
|
71
|
+
loc: [...loc],
|
|
72
|
+
msg: 'Input should be a valid email address',
|
|
73
|
+
type: 'value_error.email'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case 'number':
|
|
81
|
+
case 'int':
|
|
82
|
+
case 'integer':
|
|
83
|
+
case 'float': {
|
|
84
|
+
let num = typeof value === 'number' ? value : Number(value);
|
|
85
|
+
if (isNaN(num) || typeof value === 'boolean') {
|
|
86
|
+
errors.push({
|
|
87
|
+
loc: [...loc],
|
|
88
|
+
msg: 'Input should be a valid number',
|
|
89
|
+
type: 'number_type'
|
|
90
|
+
});
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ((expectedType === 'int' || expectedType === 'integer') && !Number.isInteger(num)) {
|
|
95
|
+
errors.push({
|
|
96
|
+
loc: [...loc],
|
|
97
|
+
msg: 'Input should be a valid integer',
|
|
98
|
+
type: 'int_type'
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof config.min === 'number' && num < config.min) {
|
|
103
|
+
errors.push({
|
|
104
|
+
loc: [...loc],
|
|
105
|
+
msg: `Input should be greater than or equal to ${config.min}`,
|
|
106
|
+
type: 'greater_than_equal',
|
|
107
|
+
ctx: { ge: config.min }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (typeof config.max === 'number' && num > config.max) {
|
|
112
|
+
errors.push({
|
|
113
|
+
loc: [...loc],
|
|
114
|
+
msg: `Input should be less than or equal to ${config.max}`,
|
|
115
|
+
type: 'less_than_equal',
|
|
116
|
+
ctx: { le: config.max }
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return num;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case 'boolean':
|
|
123
|
+
case 'bool': {
|
|
124
|
+
if (typeof value === 'boolean') return value;
|
|
125
|
+
if (value === 'true' || value === 1 || value === '1') return true;
|
|
126
|
+
if (value === 'false' || value === 0 || value === '0') return false;
|
|
127
|
+
errors.push({
|
|
128
|
+
loc: [...loc],
|
|
129
|
+
msg: 'Input should be a valid boolean',
|
|
130
|
+
type: 'bool_type'
|
|
131
|
+
});
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case 'array':
|
|
136
|
+
case 'list': {
|
|
137
|
+
if (!Array.isArray(value)) {
|
|
138
|
+
errors.push({
|
|
139
|
+
loc: [...loc],
|
|
140
|
+
msg: 'Input should be a valid array',
|
|
141
|
+
type: 'list_type'
|
|
142
|
+
});
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
if (typeof config.minItems === 'number' && value.length < config.minItems) {
|
|
146
|
+
errors.push({
|
|
147
|
+
loc: [...loc],
|
|
148
|
+
msg: `Array should have at least ${config.minItems} items`,
|
|
149
|
+
type: 'too_short',
|
|
150
|
+
ctx: { min_length: config.minItems }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (config.items) {
|
|
154
|
+
return value.map((item, index) => validateField([...loc, index], item, config.items, errors));
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case 'object': {
|
|
160
|
+
if (typeof value !== 'object' || Array.isArray(value) || value === null) {
|
|
161
|
+
errors.push({
|
|
162
|
+
loc: [...loc],
|
|
163
|
+
msg: 'Input should be a valid dictionary or object',
|
|
164
|
+
type: 'dict_type'
|
|
165
|
+
});
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
const resultObj = {};
|
|
169
|
+
const properties = config.properties || {};
|
|
170
|
+
for (const [key, propRule] of Object.entries(properties)) {
|
|
171
|
+
resultObj[key] = validateField([...loc, key], value[key], propRule, errors);
|
|
172
|
+
}
|
|
173
|
+
return resultObj;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
default:
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Validates request payload against schema.
|
|
183
|
+
*/
|
|
184
|
+
function validateSchema(schema, payload, rootLoc = 'body') {
|
|
185
|
+
const errors = [];
|
|
186
|
+
const validated = {};
|
|
187
|
+
|
|
188
|
+
const targetSchema = schema.body || schema;
|
|
189
|
+
const isTargetObject = typeof targetSchema === 'object' && targetSchema !== null;
|
|
190
|
+
|
|
191
|
+
if (!isTargetObject) {
|
|
192
|
+
return { valid: true, errors: [], data: payload || {} };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const rawPayload = (payload && typeof payload === 'object' && !Array.isArray(payload)) ? payload : {};
|
|
196
|
+
|
|
197
|
+
if (Object.keys(targetSchema).length > 0 && (!payload || typeof payload !== 'object')) {
|
|
198
|
+
errors.push({
|
|
199
|
+
loc: [rootLoc],
|
|
200
|
+
msg: 'Input should be a valid dictionary or object',
|
|
201
|
+
type: 'dict_type'
|
|
202
|
+
});
|
|
203
|
+
return { valid: false, errors, data: {} };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const [field, rule] of Object.entries(targetSchema)) {
|
|
207
|
+
const fieldVal = rawPayload[field];
|
|
208
|
+
validated[field] = validateField([rootLoc, field], fieldVal, rule, errors);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
valid: errors.length === 0,
|
|
213
|
+
errors,
|
|
214
|
+
data: validated
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Creates a standard JSON Response for Next.js / Web Fetch API environments.
|
|
220
|
+
*/
|
|
221
|
+
function createJsonResponse(data, status = 200) {
|
|
222
|
+
if (typeof Response !== 'undefined') {
|
|
223
|
+
return new Response(JSON.stringify(data), {
|
|
224
|
+
status,
|
|
225
|
+
headers: { 'Content-Type': 'application/json' }
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
// Fallback for non-Fetch mock environments
|
|
229
|
+
return {
|
|
230
|
+
status,
|
|
231
|
+
body: data,
|
|
232
|
+
headers: { 'content-type': 'application/json' }
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Next.js App Router higher-order route handler wrapper.
|
|
238
|
+
* Supports both call styles:
|
|
239
|
+
* nextAtozapi({ schema, handler })
|
|
240
|
+
* nextAtozapi({ schema: userSchema, handler: async (req, { validatedBody }) => { ... } })
|
|
241
|
+
* Or shorthand:
|
|
242
|
+
* nextAtozapi(schema, handler)
|
|
243
|
+
*
|
|
244
|
+
* @param {object|Function} optionsOrSchema
|
|
245
|
+
* @param {Function} [maybeHandler]
|
|
246
|
+
* @returns {Function} Next.js route handler (req, context) => Promise<Response>
|
|
247
|
+
*/
|
|
248
|
+
function nextAtozapi(optionsOrSchema, maybeHandler) {
|
|
249
|
+
let schema = {};
|
|
250
|
+
let handler = null;
|
|
251
|
+
|
|
252
|
+
if (typeof optionsOrSchema === 'function') {
|
|
253
|
+
handler = optionsOrSchema;
|
|
254
|
+
schema = {};
|
|
255
|
+
} else if (typeof maybeHandler === 'function') {
|
|
256
|
+
schema = optionsOrSchema || {};
|
|
257
|
+
handler = maybeHandler;
|
|
258
|
+
} else if (optionsOrSchema && typeof optionsOrSchema === 'object') {
|
|
259
|
+
schema = optionsOrSchema.schema || {};
|
|
260
|
+
handler = optionsOrSchema.handler;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (typeof handler !== 'function') {
|
|
264
|
+
throw new Error('[atozapi] Next.js route handler must be a function.');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const wrappedRoute = async function atozapiNextRoute(req, context = {}) {
|
|
268
|
+
let rawBody = {};
|
|
269
|
+
|
|
270
|
+
// In Web Request stream (Next.js App Router)
|
|
271
|
+
if (req && typeof req.json === 'function') {
|
|
272
|
+
try {
|
|
273
|
+
const cloned = req.clone ? req.clone() : req;
|
|
274
|
+
rawBody = await cloned.json();
|
|
275
|
+
} catch (err) {
|
|
276
|
+
// If content is empty or unparsable
|
|
277
|
+
rawBody = {};
|
|
278
|
+
}
|
|
279
|
+
} else if (req && req.body) {
|
|
280
|
+
rawBody = req.body;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const result = validateSchema(schema, rawBody, 'body');
|
|
284
|
+
|
|
285
|
+
if (!result.valid) {
|
|
286
|
+
return createJsonResponse({ detail: result.errors }, 422);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Attach validatedBody to request and pass directly into execution callback context
|
|
290
|
+
try {
|
|
291
|
+
req.validatedBody = result.data;
|
|
292
|
+
} catch (_) {
|
|
293
|
+
// req might be frozen in some environments
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const enhancedContext = {
|
|
297
|
+
...context,
|
|
298
|
+
validatedBody: result.data,
|
|
299
|
+
data: result.data
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
return await handler(req, enhancedContext);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Expose schema metadata for automated CLI scanner reflection
|
|
306
|
+
wrappedRoute.__atozapi = true;
|
|
307
|
+
wrappedRoute._schema = schema;
|
|
308
|
+
|
|
309
|
+
return wrappedRoute;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = {
|
|
313
|
+
nextAtozapi,
|
|
314
|
+
validateSchema,
|
|
315
|
+
validateField
|
|
316
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atozapi/nextjs",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "FastAPI-style request validation and OpenAPI metadata wrapper for Next.js App Router",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/dhirajparihar/AtoZAPI.git",
|
|
13
|
+
"directory": "sdk-js/nextjs"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/dhirajparihar/AtoZAPI#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/dhirajparihar/AtoZAPI/issues"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"index.js",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node index.js"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"atozapi",
|
|
29
|
+
"fastapi",
|
|
30
|
+
"nextjs",
|
|
31
|
+
"app-router",
|
|
32
|
+
"validation",
|
|
33
|
+
"openapi"
|
|
34
|
+
],
|
|
35
|
+
"author": "AtoZAPI Maintainers",
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|