@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -2
  3. package/index.js +316 -0
  4. 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
- # Temporary Holding Version
1
+ # @atozapi/nextjs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > FastAPI-style request validation and OpenAPI metadata wrapper for Next.js App Router route handlers.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@atozapi/nextjs.svg?style=flat-square&color=2563EB)](https://www.npmjs.com/package/@atozapi/nextjs)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }