webspresso 0.0.5 → 0.0.6

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 CHANGED
@@ -7,6 +7,7 @@ A minimal, file-based SSR framework for Node.js with Nunjucks templating.
7
7
  - **File-Based Routing**: Create pages by adding `.njk` files to a `pages/` directory
8
8
  - **Dynamic Routes**: Use `[param]` for dynamic params and `[...rest]` for catch-all routes
9
9
  - **API Endpoints**: Add `.js` files to `pages/api/` with method suffixes (e.g., `health.get.js`)
10
+ - **Schema Validation**: Zod-based request validation for body, params, and query
10
11
  - **Built-in i18n**: JSON-based translations with automatic locale detection
11
12
  - **Lifecycle Hooks**: Global and route-level hooks for request processing
12
13
  - **Template Helpers**: Laravel-inspired helper functions available in templates
@@ -494,6 +495,75 @@ Create `.js` files in `pages/api/` with optional method suffixes:
494
495
  | `pages/api/echo.post.js` | `POST /api/echo` |
495
496
  | `pages/api/users/[id].get.js` | `GET /api/users/:id` |
496
497
 
498
+ **Basic API Handler:**
499
+
500
+ ```javascript
501
+ // pages/api/health.get.js
502
+ module.exports = async function handler(req, res) {
503
+ res.json({ status: 'ok' });
504
+ };
505
+ ```
506
+
507
+ **With Schema Validation:**
508
+
509
+ ```javascript
510
+ // pages/api/posts.post.js
511
+ module.exports = {
512
+ schema: ({ z }) => ({
513
+ body: z.object({
514
+ title: z.string().min(3).max(100),
515
+ content: z.string(),
516
+ tags: z.array(z.string()).optional()
517
+ }),
518
+ query: z.object({
519
+ draft: z.coerce.boolean().default(false)
520
+ })
521
+ }),
522
+
523
+ async handler(req, res) {
524
+ // Validated & parsed data available in req.input
525
+ const { title, content, tags } = req.input.body;
526
+ const { draft } = req.input.query;
527
+
528
+ // Original req.body, req.query remain untouched
529
+ res.json({ success: true, title, draft });
530
+ }
531
+ };
532
+ ```
533
+
534
+ **Schema Options:**
535
+
536
+ | Key | Description |
537
+ |-----|-------------|
538
+ | `body` | Validates `req.body` (POST/PUT/PATCH) |
539
+ | `params` | Validates route parameters (e.g., `:id`) |
540
+ | `query` | Validates query string parameters |
541
+ | `response` | Response schema (for documentation, not enforced) |
542
+
543
+ All schemas use [Zod](https://zod.dev) for validation. Invalid requests throw a `ZodError` which can be caught by error handlers.
544
+
545
+ **Dynamic Route with Params Validation:**
546
+
547
+ ```javascript
548
+ // pages/api/users/[id].get.js
549
+ module.exports = {
550
+ schema: ({ z }) => ({
551
+ params: z.object({
552
+ id: z.string().uuid()
553
+ }),
554
+ query: z.object({
555
+ fields: z.string().optional()
556
+ })
557
+ }),
558
+
559
+ async handler(req, res) {
560
+ const { id } = req.input.params; // Validated UUID
561
+ const user = await getUser(id);
562
+ res.json(user);
563
+ }
564
+ };
565
+ ```
566
+
497
567
  ### Route Config
498
568
 
499
569
  Add a `.js` file alongside your `.njk` file to configure the route:
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Schema Applicator
3
+ * Parses and validates request input against compiled schemas
4
+ */
5
+
6
+ /**
7
+ * Apply compiled schema to request
8
+ * Parses body, params, and query against their respective schemas
9
+ * Stores validated results in req.input
10
+ *
11
+ * @param {Object} req - Express request object
12
+ * @param {Object|null} compiledSchema - Compiled schema from compileSchema
13
+ * @returns {void}
14
+ * @throws {ZodError} If validation fails
15
+ */
16
+ function applySchema(req, compiledSchema) {
17
+ // Initialize req.input
18
+ req.input = {
19
+ body: undefined,
20
+ params: undefined,
21
+ query: undefined
22
+ };
23
+
24
+ // No schema means no validation
25
+ if (!compiledSchema) {
26
+ return;
27
+ }
28
+
29
+ // Parse body if schema exists
30
+ if (compiledSchema.body) {
31
+ req.input.body = compiledSchema.body.parse(req.body);
32
+ }
33
+
34
+ // Parse params if schema exists
35
+ if (compiledSchema.params) {
36
+ req.input.params = compiledSchema.params.parse(req.params);
37
+ }
38
+
39
+ // Parse query if schema exists
40
+ if (compiledSchema.query) {
41
+ req.input.query = compiledSchema.query.parse(req.query);
42
+ }
43
+ }
44
+
45
+ module.exports = {
46
+ applySchema
47
+ };
48
+
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Schema Compiler
3
+ * Compiles schema definitions from API files
4
+ */
5
+
6
+ const z = require('zod');
7
+ const schemaCache = require('../utils/schemaCache');
8
+
9
+ /**
10
+ * Compile schema from an API module
11
+ * @param {string} filePath - Absolute file path to API module
12
+ * @param {Object} apiModule - The loaded API module
13
+ * @returns {Object|null} Compiled schema object or null if no schema
14
+ */
15
+ function compileSchema(filePath, apiModule) {
16
+ // Return cached schema if exists
17
+ if (schemaCache.has(filePath)) {
18
+ return schemaCache.get(filePath);
19
+ }
20
+
21
+ // Check if module exports schema
22
+ const schemaFn = apiModule.schema;
23
+
24
+ // Schema is optional
25
+ if (schemaFn === undefined) {
26
+ schemaCache.set(filePath, null);
27
+ return null;
28
+ }
29
+
30
+ // Schema must be a function
31
+ if (typeof schemaFn !== 'function') {
32
+ throw new Error(`Schema in ${filePath} must be a function`);
33
+ }
34
+
35
+ // Call schema function with { z }
36
+ const compiled = schemaFn({ z });
37
+
38
+ // Validate compiled schema structure
39
+ if (compiled !== null && typeof compiled !== 'object') {
40
+ throw new Error(`Schema function in ${filePath} must return an object or null`);
41
+ }
42
+
43
+ // Cache and return
44
+ schemaCache.set(filePath, compiled);
45
+ return compiled;
46
+ }
47
+
48
+ /**
49
+ * Clear schema cache for a file (for hot-reload)
50
+ * @param {string} filePath - Absolute file path
51
+ */
52
+ function invalidateSchema(filePath) {
53
+ schemaCache.del(filePath);
54
+ }
55
+
56
+ /**
57
+ * Clear all cached schemas
58
+ */
59
+ function clearAllSchemas() {
60
+ schemaCache.clear();
61
+ }
62
+
63
+ module.exports = {
64
+ compileSchema,
65
+ invalidateSchema,
66
+ clearAllSchemas
67
+ };
68
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -29,14 +29,17 @@
29
29
  "files": [
30
30
  "index.js",
31
31
  "bin/",
32
- "src/"
32
+ "src/",
33
+ "utils/",
34
+ "core/"
33
35
  ],
34
36
  "dependencies": {
35
37
  "commander": "^11.1.0",
36
38
  "express": "^4.18.2",
37
39
  "helmet": "^7.2.0",
38
40
  "inquirer": "^8.2.6",
39
- "nunjucks": "^3.2.4"
41
+ "nunjucks": "^3.2.4",
42
+ "zod": "^3.23.0"
40
43
  },
41
44
  "peerDependencies": {
42
45
  "dotenv": "^16.0.0"
@@ -449,3 +449,4 @@ module.exports = {
449
449
  matchPattern
450
450
  };
451
451
 
452
+
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Schema Cache
3
+ * Caches compiled Zod schemas per file path
4
+ */
5
+
6
+ const cache = new Map();
7
+
8
+ /**
9
+ * Get cached schema for a file path
10
+ * @param {string} filePath - Absolute file path
11
+ * @returns {Object|undefined} Compiled schema or undefined
12
+ */
13
+ function get(filePath) {
14
+ return cache.get(filePath);
15
+ }
16
+
17
+ /**
18
+ * Set compiled schema for a file path
19
+ * @param {string} filePath - Absolute file path
20
+ * @param {Object} schema - Compiled schema object
21
+ */
22
+ function set(filePath, schema) {
23
+ cache.set(filePath, schema);
24
+ }
25
+
26
+ /**
27
+ * Check if schema exists in cache
28
+ * @param {string} filePath - Absolute file path
29
+ * @returns {boolean}
30
+ */
31
+ function has(filePath) {
32
+ return cache.has(filePath);
33
+ }
34
+
35
+ /**
36
+ * Clear all cached schemas
37
+ * Useful for development hot-reload
38
+ */
39
+ function clear() {
40
+ cache.clear();
41
+ }
42
+
43
+ /**
44
+ * Delete a specific schema from cache
45
+ * @param {string} filePath - Absolute file path
46
+ * @returns {boolean} True if deleted
47
+ */
48
+ function del(filePath) {
49
+ return cache.delete(filePath);
50
+ }
51
+
52
+ module.exports = {
53
+ get,
54
+ set,
55
+ has,
56
+ clear,
57
+ del
58
+ };
59
+