nuxt-procedures 0.1.2

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 ADDED
@@ -0,0 +1,203 @@
1
+ # Nuxt Procedures
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![License][license-src]][license-href]
6
+ [![Nuxt][nuxt-src]][nuxt-href]
7
+
8
+ Nuxt module to define and easily consume your backend API in a validated and
9
+ type-safe way using procedures and Zod schemas.
10
+
11
+ - [🏀 Online playground](https://stackblitz.com/github/andresberrios/nuxt-procedures?file=playground%2Fapp.vue)
12
+
13
+ ## Features
14
+
15
+ - **Type-Safe API Layer:** End-to-end type safety for your API calls.
16
+ - **Zod Validation:** Use Zod schemas to validate inputs and outputs.
17
+ - **Automatic API Client:** An `apiClient` is automatically generated based on your procedures.
18
+ - **`useFetch` Integration:** Seamlessly integrates with Nuxt's `useFetch` for easy data fetching in your components.
19
+ - **`superjson` Support:** Automatically handles serialization of complex data types.
20
+
21
+ ## Installation
22
+
23
+ ### Quick Install (Recommended)
24
+
25
+ Install and configure the module with a single command:
26
+
27
+ ```bash
28
+ npx nuxi module add nuxt-procedures
29
+ ```
30
+ This will add `nuxt-procedures` to your `package.json` and `nuxt.config.ts`.
31
+
32
+ You also need to install its peer dependency, `zod`:
33
+ ```bash
34
+ npm install zod
35
+ ```
36
+
37
+ ### Manual Install
38
+
39
+ 1. Install the packages:
40
+
41
+ ```bash
42
+ npm install nuxt-procedures zod
43
+ ```
44
+
45
+ 2. Add the module to your `nuxt.config.ts`:
46
+
47
+ ```typescript
48
+ export default defineNuxtConfig({
49
+ modules: ['nuxt-procedures'],
50
+ })
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ ### 1. Define Procedures
56
+
57
+ Create `.ts` files in your `server/api` directory. The module will automatically create a corresponding client for each file.
58
+
59
+ #### Simple Example
60
+
61
+ For a simple input and output, you can use Zod schemas directly.
62
+
63
+ `server/api/hello.ts`:
64
+ ```typescript
65
+ import { z } from 'zod'
66
+
67
+ export default defineProcedure({
68
+ input: z.string(),
69
+ output: z.string(),
70
+ handler: async ({ input }) => {
71
+ return `Hello, ${input}!`
72
+ },
73
+ })
74
+ ```
75
+
76
+ #### Complex Example
77
+
78
+ For more complex scenarios, `z.object` is the way to go. This is useful for things like form submissions or creating database entries.
79
+
80
+ `server/api/users/create.ts`:
81
+ ```typescript
82
+ import { z } from 'zod'
83
+
84
+ export default defineProcedure({
85
+ input: z.object({
86
+ name: z.string(),
87
+ email: z.string().email(),
88
+ role: z.enum(['admin', 'user']).default('user'),
89
+ }),
90
+ output: z.object({
91
+ id: z.string(),
92
+ name: z.string(),
93
+ email: z.string(),
94
+ }),
95
+ handler: async ({ input, event }) => {
96
+ // The event param gives you access to the request context and Nuxt utilities
97
+ // For example, you can access request headers
98
+ const headers = getRequestHeaders(event)
99
+ console.log(headers)
100
+
101
+ // In a real app, you would create a user in your database
102
+ // The following is just an example of how you could get a db client
103
+ const db = await useDB(event)
104
+ const newUser = await db.user.create({
105
+ data: input,
106
+ })
107
+
108
+ return newUser
109
+ },
110
+ })
111
+ ```
112
+
113
+ ### 2. Use the `apiClient`
114
+
115
+ The module automatically generates an `apiClient` that you can use in your components or pages. The structure of the `apiClient` mirrors your `server/api` directory.
116
+
117
+ #### `useCall`
118
+
119
+ The `useCall` method is a wrapper around Nuxt's `useFetch` and is the recommended way to call procedures from your Vue components.
120
+
121
+ Calling the simple `hello` procedure:
122
+ ```vue
123
+ <script setup lang="ts">
124
+ const { data: greeting, pending } = await apiClient.hello.useCall('World')
125
+ </script>
126
+
127
+ <template>
128
+ <p v-if="pending">Loading...</p>
129
+ <p v-else>{{ greeting }}</p>
130
+ </template>
131
+ ```
132
+
133
+ Calling the complex `users/create` procedure:
134
+ ```vue
135
+ <script setup lang="ts">
136
+ const { data: newUser, execute } = apiClient.users.create.useCall({
137
+ name: 'Andres',
138
+ email: 'andres@example.com',
139
+ })
140
+
141
+ // `execute` can be called later, e.g. in a form submission handler
142
+ // const submitForm = () => execute()
143
+ </script>
144
+ ```
145
+
146
+ #### `call`
147
+
148
+ The `call` method makes a direct API call and is useful for calling procedures from server-side code or when you don't need the features of `useFetch`.
149
+
150
+ ```typescript
151
+ // Calling the simple procedure
152
+ const greeting = await apiClient.hello.call('World')
153
+ // greeting is "Hello, World!"
154
+
155
+ // Calling the complex procedure
156
+ const newUser = await apiClient.users.create.call({
157
+ name: 'Andres',
158
+ email: 'andres@example.com',
159
+ })
160
+ // newUser is { id: '...', name: 'Andres', email: 'andres@example.com' }
161
+ ```
162
+
163
+ ## Contribution
164
+
165
+ <details>
166
+ <summary>Local development</summary>
167
+
168
+ ```bash
169
+ # Install dependencies
170
+ npm install
171
+
172
+ # Generate type stubs
173
+ npm run dev:prepare
174
+
175
+ # Develop with the playground
176
+ npm run dev
177
+
178
+ # Build the playground
179
+ npm run dev:build
180
+
181
+ # Run ESLint
182
+ npm run lint
183
+
184
+ # Run Vitest
185
+ npm run test
186
+ npm run test:watch
187
+
188
+ # Release new version
189
+ npm run release
190
+ ```
191
+
192
+ </details>
193
+
194
+ <!-- Badges -->
195
+
196
+ [npm-version-src]: https://img.shields.io/npm/v/nuxt-procedures/latest.svg?style=flat&colorA=020420&colorB=00DC82
197
+ [npm-version-href]: https://npmjs.com/package/nuxt-procedures
198
+ [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-procedures.svg?style=flat&colorA=020420&colorB=00DC82
199
+ [npm-downloads-href]: https://npm.chart.dev/nuxt-procedures
200
+ [license-src]: https://img.shields.io/npm/l/nuxt-procedures.svg?style=flat&colorA=020420&colorB=00DC82
201
+ [license-href]: https://npmjs.com/package/nuxt-procedures
202
+ [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js
203
+ [nuxt-href]: https://nuxt.com
@@ -0,0 +1,9 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ enabled: boolean;
5
+ }
6
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
7
+
8
+ export { _default as default };
9
+ export type { ModuleOptions };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "nuxt-procedures",
3
+ "configKey": "procedures",
4
+ "version": "0.1.2",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.2",
7
+ "unbuild": "3.6.0"
8
+ }
9
+ }
@@ -0,0 +1,113 @@
1
+ import { join, parse, sep } from 'node:path';
2
+ import { defineNuxtModule, createResolver, addServerImports, addTemplate, updateTemplates, addImports, resolveFiles } from '@nuxt/kit';
3
+ import { camelCase } from 'scule';
4
+
5
+ const toCode = Symbol("toCode");
6
+ const toList = Symbol("toList");
7
+ class Procedure {
8
+ constructor(file, url) {
9
+ this.file = file;
10
+ this.url = url;
11
+ }
12
+ id = "proc" + Math.random().toString(36).slice(2);
13
+ [toCode]() {
14
+ return `createCaller<typeof ${this.id}>("${this.url}")`;
15
+ }
16
+ [toList]() {
17
+ return [this];
18
+ }
19
+ }
20
+ class Router {
21
+ [toCode]() {
22
+ return `{
23
+ ${Object.entries(this).map(([key, value]) => `"${camelCase(key)}": ${value[toCode]()}`).join(",\n")}
24
+ }`;
25
+ }
26
+ [toList]() {
27
+ return Object.values(this).flatMap((v) => v[toList]());
28
+ }
29
+ }
30
+ const module = defineNuxtModule({
31
+ meta: {
32
+ name: "nuxt-procedures",
33
+ configKey: "procedures"
34
+ },
35
+ // Default configuration options of the Nuxt module
36
+ defaults: {
37
+ enabled: true
38
+ },
39
+ async setup(options, nuxt) {
40
+ if (!options.enabled) {
41
+ return;
42
+ }
43
+ const { resolve } = createResolver(import.meta.url);
44
+ addServerImports([
45
+ {
46
+ from: resolve("./runtime/define-procedure.ts"),
47
+ name: "defineProcedure"
48
+ }
49
+ ]);
50
+ const createCallerPath = resolve("./runtime/create-caller.ts");
51
+ const generatedClient = addTemplate({
52
+ filename: "nuxt-procedures/api-client.ts",
53
+ getContents: () => generateClientCode(nuxt, createCallerPath),
54
+ write: true
55
+ });
56
+ nuxt.hook("builder:watch", async (event, path) => {
57
+ if (path.includes("api")) {
58
+ updateTemplates({
59
+ filter: (t) => t.filename === generatedClient.filename
60
+ });
61
+ }
62
+ });
63
+ const imports = [{ from: generatedClient.dst, name: "apiClient" }];
64
+ addImports(imports);
65
+ }
66
+ });
67
+ async function buildRouterStructure(nuxt) {
68
+ const procedureDirs = nuxt.options._layers.map(
69
+ (l) => (
70
+ // join(l.config.serverDir ?? "server", "procedures")
71
+ join(l.config.serverDir ?? "server", "api")
72
+ )
73
+ );
74
+ const router = new Router();
75
+ for (const procDir of procedureDirs) {
76
+ const files = await resolveFiles(procDir, "**/*.ts");
77
+ for (const file of files) {
78
+ const { name, dir } = parse(file);
79
+ const namespace = dir.slice(procDir.length + 1);
80
+ const url = namespace === "" ? `/${name}` : `/${namespace}/${name}`;
81
+ if (namespace === "") {
82
+ router[name] = new Procedure(file, url);
83
+ } else {
84
+ const parts = namespace.split(sep);
85
+ let subRouter = router;
86
+ for (const part of parts) {
87
+ if (!subRouter[part] || subRouter[part] instanceof Procedure) {
88
+ subRouter[part] = new Router();
89
+ }
90
+ subRouter = subRouter[part];
91
+ }
92
+ subRouter[name] = new Procedure(file, url);
93
+ }
94
+ }
95
+ }
96
+ return router;
97
+ }
98
+ function withoutExtension(file) {
99
+ return file.slice(0, -parse(file).ext.length);
100
+ }
101
+ async function generateClientCode(nuxt, createCallerPath) {
102
+ const router = await buildRouterStructure(nuxt);
103
+ const procedures = router[toList]();
104
+ return `
105
+ import { createCaller } from "${withoutExtension(createCallerPath)}";
106
+
107
+ ${procedures.map((p) => `import type ${p.id} from "${withoutExtension(p.file)}";`).join("\n")}
108
+
109
+ export const apiClient = ${router[toCode]()};
110
+ `;
111
+ }
112
+
113
+ export { module as default };
@@ -0,0 +1,16 @@
1
+ import type { z } from 'zod';
2
+ import type { Procedure } from './define-procedure.js';
3
+ import { useFetch } from '#imports';
4
+ type InferProcedureInput<P> = P extends Procedure<infer I, any> ? I extends z.ZodUndefined ? unknown : z.infer<I> : never;
5
+ type InferProcedureOutput<P> = P extends Procedure<any, infer O> ? O extends z.ZodTypeAny ? z.infer<O> : unknown : never;
6
+ type Call<P extends Procedure<any, any>> = [unknown] extends [
7
+ InferProcedureInput<P>
8
+ ] ? () => Promise<InferProcedureOutput<P>> : (input: InferProcedureInput<P>) => Promise<InferProcedureOutput<P>>;
9
+ type UseCall<P extends Procedure<any, any>> = [unknown] extends [
10
+ InferProcedureInput<P>
11
+ ] ? () => ReturnType<typeof useFetch<InferProcedureOutput<P>>> : (input: InferProcedureInput<P>) => ReturnType<typeof useFetch<InferProcedureOutput<P>>>;
12
+ export declare function createCaller<P extends Procedure<any, any>>(url: string): {
13
+ call: Call<P>;
14
+ useCall: UseCall<P>;
15
+ };
16
+ export {};
@@ -0,0 +1,23 @@
1
+ import superjson from "superjson";
2
+ import { useFetch, useRequestHeaders } from "#imports";
3
+ export function createCaller(url) {
4
+ return {
5
+ call: (input) => {
6
+ return $fetch("/api" + url, {
7
+ method: "POST",
8
+ body: superjson.serialize(input),
9
+ parseResponse: superjson.parse,
10
+ headers: useRequestHeaders(["cookie"])
11
+ });
12
+ },
13
+ useCall: (input) => {
14
+ return useFetch("/api" + url, {
15
+ key: `${url}: ${JSON.stringify(input)}`,
16
+ method: "POST",
17
+ body: superjson.serialize(input),
18
+ parseResponse: superjson.parse,
19
+ headers: useRequestHeaders(["cookie"])
20
+ });
21
+ }
22
+ };
23
+ }
@@ -0,0 +1,22 @@
1
+ import type { EventHandler, H3Event } from 'h3';
2
+ import z from 'zod';
3
+ export interface Procedure<I extends z.ZodTypeAny = z.ZodUndefined, O extends z.ZodTypeAny = z.ZodVoid> extends EventHandler {
4
+ __procedureMetadata: {
5
+ inputSchema: I;
6
+ outputSchema: O;
7
+ };
8
+ }
9
+ type Awaitable<T> = T | Promise<T>;
10
+ type HandlerArgs<I> = I extends undefined ? {
11
+ event: H3Event;
12
+ } : {
13
+ input: I;
14
+ event: H3Event;
15
+ };
16
+ type Handler<I extends z.ZodTypeAny = z.ZodUndefined, O extends z.ZodTypeAny = z.ZodVoid> = (args: HandlerArgs<z.infer<I>>) => Awaitable<z.infer<O>>;
17
+ export declare function defineProcedure<I extends z.ZodTypeAny = z.ZodUndefined, O extends z.ZodTypeAny = z.ZodVoid>(args: {
18
+ input?: I;
19
+ output?: O;
20
+ handler: Handler<I, O>;
21
+ }): Procedure<I, O>;
22
+ export {};
@@ -0,0 +1,17 @@
1
+ import z from "zod";
2
+ import superjson from "superjson";
3
+ export function defineProcedure({
4
+ input: inputSchema,
5
+ output: outputSchema = z.void(),
6
+ handler
7
+ }) {
8
+ const h = defineEventHandler(async (event) => {
9
+ const input = inputSchema ? inputSchema.parse(superjson.deserialize(await readBody(event))) : void 0;
10
+ const outputRaw = await handler(inputSchema ? { input, event } : { event });
11
+ const output = outputSchema.parse(outputRaw);
12
+ return superjson.serialize(output);
13
+ });
14
+ const procedure = h;
15
+ procedure.__procedureMetadata = { inputSchema, outputSchema };
16
+ return procedure;
17
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json",
3
+ }
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "nuxt-procedures",
3
+ "version": "0.1.2",
4
+ "description": "Nuxt module to define and easily consume your backend API in a validated and type-safe way using procedures and Zod schemas.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/andresberrios/nuxt-procedures.git"
8
+ },
9
+ "license": "MIT",
10
+ "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/types.d.mts",
14
+ "import": "./dist/module.mjs"
15
+ }
16
+ },
17
+ "main": "./dist/module.mjs",
18
+ "typesVersions": {
19
+ "*": {
20
+ ".": [
21
+ "./dist/types.d.mts"
22
+ ]
23
+ }
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "scripts": {
29
+ "prepack": "nuxt-module-build build",
30
+ "dev": "npm run dev:prepare && nuxi dev playground",
31
+ "dev:build": "nuxi build playground",
32
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
33
+ "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
34
+ "lint": "eslint .",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest watch",
37
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
38
+ },
39
+ "dependencies": {
40
+ "@nuxt/kit": "^4.0.1",
41
+ "scule": "^1.3.0",
42
+ "superjson": "^2.2.2"
43
+ },
44
+ "peerDependencies": {
45
+ "zod": "^3.0.0 || ^4.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@nuxt/devtools": "^2.6.2",
49
+ "@nuxt/eslint-config": "^1.7.1",
50
+ "@nuxt/module-builder": "^1.0.2",
51
+ "@nuxt/schema": "^4.0.1",
52
+ "@nuxt/test-utils": "^3.19.2",
53
+ "@types/node": "latest",
54
+ "changelogen": "^0.6.2",
55
+ "eslint": "^9.32.0",
56
+ "nuxt": "^4.0.1",
57
+ "typescript": "~5.8.3",
58
+ "vitest": "^3.2.4",
59
+ "vue-tsc": "^3.0.4"
60
+ }
61
+ }