typeowl-example-backend 1.0.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 ADDED
@@ -0,0 +1,68 @@
1
+ <p align="center">
2
+ <img src="../frontend/public/typeOwl.logo.png" alt="TypeOwl Logo" width="120" />
3
+ </p>
4
+
5
+ # TypeOwl Example Backend
6
+
7
+ A Fastify API server that exposes types via TypeOwl.
8
+
9
+ ## Setup
10
+
11
+ ```bash
12
+ npm install
13
+ npm run dev
14
+ ```
15
+
16
+ The server starts on `http://localhost:3001`.
17
+
18
+ ## API Endpoints
19
+
20
+ | Method | Path | Description |
21
+ |--------|------|-------------|
22
+ | GET | `/api/users` | List all users |
23
+ | GET | `/api/users/:id` | Get user by ID |
24
+ | POST | `/api/users` | Create a new user |
25
+ | DELETE | `/api/users/:id` | Delete a user |
26
+ | GET | `/api/posts` | List all posts |
27
+ | GET | `/api/posts/:id` | Get post by ID |
28
+ | POST | `/api/posts` | Create a new post |
29
+
30
+ ## TypeOwl Endpoints
31
+
32
+ | Path | Description |
33
+ |------|-------------|
34
+ | `/__typeowl` | JSON manifest (metadata + file pointers) |
35
+ | `/__typeowl/types/users.d.ts` | User domain types |
36
+ | `/__typeowl/types/posts.d.ts` | Post domain types |
37
+ | `/__typeowl/types/common.d.ts` | Shared types (ApiError, etc.) |
38
+ | `/__typeowl/types/index.d.ts` | All types + ApiEndpoints |
39
+
40
+ ## How It Works
41
+
42
+ ```typescript
43
+ import { createTypeOwl } from 'typeowl/server';
44
+ import { z } from 'zod';
45
+
46
+ const typeowl = createTypeOwl({ version: '1.0.0' });
47
+
48
+ // Register types by domain
49
+ typeowl
50
+ .domain('users')
51
+ .registerZod('User', UserSchema)
52
+ .registerZod('CreateUserInput', CreateUserSchema);
53
+
54
+ // Register endpoints
55
+ typeowl
56
+ .domain('main')
57
+ .get('/api/users', 'User[]')
58
+ .post('/api/users', 'User', { body: 'CreateUserInput' });
59
+
60
+ // Handle TypeOwl requests in your server
61
+ app.get('/__typeowl/*', (req, reply) => {
62
+ const response = typeowl.handleRequest(req.url);
63
+ if (response) {
64
+ return reply.type(response.contentType).send(response.body);
65
+ }
66
+ });
67
+ ```
68
+
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "typeowl-example-backend",
3
+ "version": "1.0.0",
4
+ "description": "Example backend using TypeOwl for type sharing",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/server.ts",
8
+ "start": "node dist/server.js",
9
+ "build": "tsc"
10
+ },
11
+ "dependencies": {
12
+ "@fastify/cors": "^9.0.1",
13
+ "fastify": "^4.26.0",
14
+ "typeowl": "file:../..",
15
+ "zod": "^4.3.5"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.10.0",
19
+ "tsx": "^4.7.0",
20
+ "typescript": "^5.3.0"
21
+ }
22
+ }
package/src/server.ts ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * 🦉 TypeOwl Example Backend
3
+ *
4
+ * Demonstrates THREE ways to define API types:
5
+ *
6
+ * 🔴 OPTION 1: No TypeOwl - Raw handlers, no type exposure
7
+ * → Frontend uses `any` or manual types
8
+ *
9
+ * 🟡 OPTION 2: Static types only - Types extracted from src/types/
10
+ * → Frontend can force-cast using exposed static types
11
+ *
12
+ * 🟢 OPTION 3: typeowl.endpoint() - Full type generation + validation
13
+ * → Frontend gets auto-generated endpoint types
14
+ *
15
+ * Run: npm run dev
16
+ */
17
+
18
+ import Fastify from 'fastify';
19
+ import cors from '@fastify/cors';
20
+ import { z } from 'zod';
21
+ import { initTypeOwl } from 'typeowl/server';
22
+
23
+ // Import types from our types folder (for use in this file)
24
+ import type { Blog, Product } from './types/index.js';
25
+
26
+ // ═══════════════════════════════════════════════════════════════════════════
27
+ // 🦉 TYPEOWL - Auto-loads from typeowl.server.config.ts
28
+ // ═══════════════════════════════════════════════════════════════════════════
29
+
30
+ const typeowl = await initTypeOwl();
31
+
32
+ // ═══════════════════════════════════════════════════════════════════════════
33
+ // 📦 ZOD SCHEMAS (for typeowl.endpoint validation)
34
+ // ═══════════════════════════════════════════════════════════════════════════
35
+
36
+ // Common params schema
37
+ const IdParamsSchema = z.object({ id: z.string() });
38
+
39
+ // User schemas
40
+ const UserSchema = z.object({
41
+ id: z.string(),
42
+ email: z.string().email(),
43
+ name: z.string(),
44
+ role: z.enum(['admin', 'user', 'guest']),
45
+ });
46
+
47
+ const CreateUserSchema = z.object({
48
+ email: z.string().email(),
49
+ name: z.string(),
50
+ role: z.enum(['admin', 'user', 'guest']).default('user'),
51
+ });
52
+
53
+ // Health check (untyped endpoint)
54
+ const HealthSchema = z.object({
55
+ status: z.string(),
56
+ timestamp: z.string(),
57
+ });
58
+
59
+ // ═══════════════════════════════════════════════════════════════════════════
60
+ // 🌐 FASTIFY SERVER
61
+ // ═══════════════════════════════════════════════════════════════════════════
62
+
63
+ const app = Fastify({ logger: false });
64
+
65
+ await app.register(cors, { origin: true });
66
+
67
+ // Mount TypeOwl routes (from config)
68
+ await typeowl.mount(app);
69
+
70
+ // ─── Sample Data ─────────────────────────────────────────────────────────────
71
+
72
+ const blogs: Blog[] = [
73
+ { id: '1', title: 'Hello World', content: 'Welcome to TypeOwl!', published: true, createdAt: new Date().toISOString() },
74
+ { id: '2', title: 'Getting Started', content: 'Let me show you how...', published: true, createdAt: new Date().toISOString() },
75
+ ];
76
+
77
+ const products: Product[] = [
78
+ { id: '1', name: 'TypeOwl Pro', price: 99, description: 'Full-featured type sharing', inStock: true },
79
+ { id: '2', name: 'TypeOwl Starter', price: 0, inStock: true },
80
+ ];
81
+
82
+ type User = z.infer<typeof UserSchema>;
83
+ const users: User[] = [
84
+ { id: '1', email: 'alice@example.com', name: 'Alice', role: 'admin' },
85
+ { id: '2', email: 'bob@example.com', name: 'Bob', role: 'user' },
86
+ ];
87
+
88
+ // ═══════════════════════════════════════════════════════════════════════════
89
+ // 🔴 OPTION 1: No TypeOwl - Raw Fastify handlers
90
+ // ═══════════════════════════════════════════════════════════════════════════
91
+ // These endpoints are NOT registered with TypeOwl.
92
+ // Frontend must use `any` or define types manually.
93
+
94
+ app.get('/api/health', async () => {
95
+ return { status: 'ok', timestamp: new Date().toISOString() };
96
+ });
97
+
98
+ // ═══════════════════════════════════════════════════════════════════════════
99
+ // 🟡 OPTION 2: Static types only - Types exposed via extract config
100
+ // ═══════════════════════════════════════════════════════════════════════════
101
+ // Blog type is extracted from src/types/Blog.ts via typeowl.server.config.ts
102
+ // The TYPE is exposed, but endpoints are NOT mapped.
103
+ // Frontend can force-cast responses using the exposed Blog type.
104
+
105
+ app.get('/api/blogs', async () => blogs);
106
+
107
+ app.get('/api/blogs/:id', async (request, reply) => {
108
+ const { id } = request.params as { id: string };
109
+ const blog = blogs.find(b => b.id === id);
110
+ if (!blog) return reply.code(404).send({ error: 'Blog not found' });
111
+ return blog;
112
+ });
113
+
114
+ // ═══════════════════════════════════════════════════════════════════════════
115
+ // 🟢 OPTION 3: typeowl.endpoint() - Full type generation + validation
116
+ // ═══════════════════════════════════════════════════════════════════════════
117
+ // These endpoints have AUTOMATIC validation and type registration!
118
+ // Schemas are extracted to TypeOwl AND used for runtime validation.
119
+ // Frontend gets fully typed ApiEndpoints with body/params/response.
120
+
121
+ // GET /api/users - List all users
122
+ typeowl.endpoint(app, 'GET', '/api/users', {
123
+ response: z.array(UserSchema),
124
+ description: 'List all users',
125
+ }, async () => {
126
+ return users;
127
+ });
128
+
129
+ // GET /api/users/:id - Get user by ID
130
+ typeowl.endpoint(app, 'GET', '/api/users/:id', {
131
+ params: IdParamsSchema,
132
+ response: UserSchema,
133
+ description: 'Get user by ID',
134
+ }, async ({ params, reply }) => {
135
+ const user = users.find(u => u.id === params.id);
136
+ if (!user) {
137
+ (reply as { code: (n: number) => { send: (d: unknown) => void } }).code(404).send({ error: 'User not found' });
138
+ return undefined as never;
139
+ }
140
+ return user;
141
+ });
142
+
143
+ // POST /api/users - Create user (with body validation!)
144
+ typeowl.endpoint(app, 'POST', '/api/users', {
145
+ body: CreateUserSchema,
146
+ response: UserSchema,
147
+ description: 'Create a new user',
148
+ }, async ({ body }) => {
149
+ // body is already validated and typed as { email: string, name: string, role: 'admin'|'user'|'guest' }
150
+ const newUser: User = {
151
+ id: String(users.length + 1),
152
+ email: body.email,
153
+ name: body.name,
154
+ role: body.role,
155
+ };
156
+ users.push(newUser);
157
+ return newUser;
158
+ });
159
+
160
+ // DELETE /api/users/:id - Delete user
161
+ typeowl.endpoint(app, 'DELETE', '/api/users/:id', {
162
+ params: IdParamsSchema,
163
+ response: z.object({ success: z.boolean(), message: z.string() }),
164
+ description: 'Delete a user',
165
+ }, async ({ params, reply }) => {
166
+ const index = users.findIndex(u => u.id === params.id);
167
+ if (index === -1) {
168
+ (reply as { code: (n: number) => { send: (d: unknown) => void } }).code(404).send({ error: 'User not found' });
169
+ return undefined as never;
170
+ }
171
+ users.splice(index, 1);
172
+ return { success: true, message: 'User deleted' };
173
+ });
174
+ // POST /api/blogs - Create a new blog
175
+ typeowl.endpoint(app, 'POST', '/api/blogs', {
176
+ body:'BlogInput',
177
+ response: 'Blog',
178
+ description: 'Create a new blog',
179
+ }, async ({ body }) => console.log(body));
180
+
181
+ // GET /api/products - List all products
182
+ typeowl.endpoint(app, 'GET', '/api/products', {
183
+ response: 'Product[]',
184
+ description: 'List all products',
185
+ }, async () => products);
186
+
187
+ // GET /api/products/:id - Get product by ID
188
+ typeowl.endpoint(app, 'GET', '/api/products/:id', {
189
+ params: IdParamsSchema,
190
+ response: 'Product | null', // Using extracted type reference!
191
+ description: 'Get product by ID',
192
+ }, async ({ params, reply }) => {
193
+ const product = products.find(p => p.id === params.id);
194
+ if (!product) {
195
+ (reply as { code: (n: number) => { send: (d: unknown) => void } }).code(404).send({ error: 'Product not found' });
196
+ return undefined as never;
197
+ }
198
+ return product;
199
+ });
200
+
201
+ // ─── Start Server ────────────────────────────────────────────────────────────
202
+
203
+ const PORT = 3001;
204
+
205
+ app.listen({ port: PORT, host: '0.0.0.0' }, (err) => {
206
+ if (err) {
207
+ console.error(err);
208
+ process.exit(1);
209
+ }
210
+
211
+ console.log(`
212
+ \x1b[36m🦉 TypeOwl Example Backend\x1b[0m
213
+ \x1b[2m──────────────────────────────\x1b[0m
214
+
215
+ Server: \x1b[32mhttp://localhost:${PORT}\x1b[0m
216
+
217
+ \x1b[31m🔴 No TypeOwl (raw handlers):\x1b[0m
218
+ GET /api/health
219
+
220
+ \x1b[33m🟡 Static types only (Blog exposed, endpoint not mapped):\x1b[0m
221
+ GET /api/blogs
222
+ GET /api/blogs/:id
223
+
224
+ \x1b[32m🟢 typeowl.endpoint() (full type + validation):\x1b[0m
225
+ GET /api/products
226
+ GET /api/products/:id
227
+ GET /api/users
228
+ GET /api/users/:id
229
+ POST /api/users \x1b[33m← Try invalid body!\x1b[0m
230
+ DELETE /api/users/:id
231
+
232
+ TypeOwl Endpoints:
233
+ \x1b[33mhttp://localhost:${PORT}/__typeowl\x1b[0m
234
+ \x1b[33mhttp://localhost:${PORT}/__typeowl/types/content.d.ts\x1b[0m
235
+ \x1b[33mhttp://localhost:${PORT}/__typeowl/types/endpoints.d.ts\x1b[0m
236
+
237
+ \x1b[2mPress Ctrl+C to stop\x1b[0m
238
+ `);
239
+ });
@@ -0,0 +1,11 @@
1
+ import { Author } from "./anothertype.js";
2
+
3
+ export type Blog = {
4
+ id: string;
5
+ title: string;
6
+ content: string;
7
+ published: boolean;
8
+ createdAt: string;
9
+ author?: Author;
10
+ }
11
+ export type BlogInput = Pick<Blog, 'title' | 'content'>;
@@ -0,0 +1,7 @@
1
+ export interface Product {
2
+ id: string;
3
+ name: string;
4
+ price: number;
5
+ description?: string;
6
+ inStock: boolean;
7
+ }
@@ -0,0 +1,22 @@
1
+ type Developer = {
2
+ id: string;
3
+ name: string;
4
+ email: string;
5
+ role: DeveloperRole;
6
+ };
7
+ enum DeveloperRole {
8
+ ADMIN = 'admin',
9
+ USER = 'user',
10
+ GUEST = 'guest',
11
+ }
12
+ type customeType<T extends string | number> = `custom_${T}`;
13
+ type Author = {
14
+ isDeveloper: boolean;
15
+ developers: Map<string, Developer>;
16
+ customType: customeType<string>;
17
+ };
18
+ type NoUsageType = {
19
+ id: string;
20
+ noUsage: boolean;
21
+ };
22
+ export type { Author , NoUsageType};
@@ -0,0 +1,5 @@
1
+ import type { Product } from './Product.ts';
2
+ import type { Blog ,BlogInput } from './Blog.ts';
3
+
4
+
5
+ export type { Blog, Product , BlogInput};
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src"
11
+ },
12
+ "include": ["src/**/*"]
13
+ }
14
+
@@ -0,0 +1,201 @@
1
+ /**
2
+ * 🦉 TypeOwl Server Configuration
3
+ *
4
+ * This is the central configuration file for TypeOwl.
5
+ * All settings for type extraction, serving, and security are defined here.
6
+ */
7
+
8
+ import { defineServerConfig } from 'typeowl/server';
9
+ import type { FastifyInstance } from 'fastify';
10
+
11
+ // Helper to cast app to your framework type
12
+ const asFastify = (app: unknown) => app as FastifyInstance;
13
+
14
+ export default defineServerConfig({
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+ // 📍 BASE CONFIGURATION
17
+ // ═══════════════════════════════════════════════════════════════════════════
18
+
19
+ /**
20
+ * Base path for TypeOwl endpoints.
21
+ * All type endpoints will be served under this path.
22
+ *
23
+ * @default '/__typeowl'
24
+ * @example '/__typeowl' -> http://localhost:3001/__typeowl/types/content.d.ts
25
+ */
26
+ basePath: '/__typeowl',
27
+
28
+ /**
29
+ * Version string for cache invalidation.
30
+ * Change this when your types change to bust client caches.
31
+ *
32
+ * @example '1.0.0', 'v2', process.env.npm_package_version
33
+ */
34
+ version: '1.0.0',
35
+
36
+ /**
37
+ * Include git commit hash in the manifest.
38
+ * Useful for debugging which version is deployed.
39
+ *
40
+ * @default false
41
+ */
42
+ includeGitCommit: false,
43
+
44
+ // ═══════════════════════════════════════════════════════════════════════════
45
+ // 📦 TYPE EXTRACTION
46
+ // ═══════════════════════════════════════════════════════════════════════════
47
+
48
+ /**
49
+ * 🔒 Allowed source files/directories for type extraction.
50
+ * Only files within these paths can have their types extracted.
51
+ * This is a security measure to prevent accidental exposure of sensitive types.
52
+ *
53
+ * Can be:
54
+ * - Single file: './src/types.ts'
55
+ * - Single directory: './src/types/'
56
+ * - Multiple paths: ['./src/types/', './src/models/']
57
+ *
58
+ * ⚠️ Required for extractAndRegister() to work.
59
+ */
60
+ typeSources: './src/types/',
61
+
62
+ /**
63
+ * Types to extract from source files.
64
+ * Maps domain names to source locations and type names.
65
+ *
66
+ * Each domain becomes a separate .d.ts file:
67
+ * - content -> /__typeowl/types/content.d.ts
68
+ * - models -> /__typeowl/types/models.d.ts
69
+ *
70
+ * @example
71
+ * extract: {
72
+ * content: { from: './src/types/', types: ['Blog', 'Product'] },
73
+ * models: { from: './src/models/', types: ['User', 'Order'] },
74
+ * }
75
+ */
76
+ extract: {
77
+ content: {
78
+ from: './src/types/',
79
+ types: '*', // Extract ALL exported types automatically!
80
+ },
81
+ },
82
+
83
+ // ═══════════════════════════════════════════════════════════════════════════
84
+ // 🛡️ GUARD
85
+ // ═══════════════════════════════════════════════════════════════════════════
86
+
87
+ /**
88
+ * Guard settings for TypeOwl endpoints.
89
+ */
90
+ guard: {
91
+ /**
92
+ * When to enable TypeOwl endpoints:
93
+ * - true: Always enabled (use with caution in production!)
94
+ * - false: Always disabled
95
+ * - 'development': Only when NODE_ENV !== 'production'
96
+ *
97
+ * @default 'development'
98
+ */
99
+ enabled: 'development',
100
+
101
+ /**
102
+ * API key required to access type endpoints.
103
+ * If set, clients must include the key via:
104
+ * - Header: X-TypeOwl-Key: your-api-key
105
+ * - Query param: ?key=your-api-key
106
+ *
107
+ * @example process.env.TYPEOWL_API_KEY
108
+ */
109
+ // apiKey: process.env.TYPEOWL_API_KEY,
110
+ },
111
+
112
+ // ═══════════════════════════════════════════════════════════════════════════
113
+ // 🚀 SERVING MODE
114
+ // ═══════════════════════════════════════════════════════════════════════════
115
+
116
+ /**
117
+ * How TypeOwl serves types:
118
+ *
119
+ * - 'dynamic': Types served at runtime via registerRoutes.
120
+ * Your server handles requests to /__typeowl/*.
121
+ * Requires registerRoutes to be defined.
122
+ *
123
+ * - 'static': Types generated as static files by CLI.
124
+ * Run: npx typeowl generate
125
+ * Files output to basePath (e.g., ./public/__typeowl/).
126
+ * registerRoutes is optional (not needed for static hosting).
127
+ *
128
+ * @default 'dynamic'
129
+ */
130
+ mode: 'dynamic',
131
+
132
+ // ═══════════════════════════════════════════════════════════════════════════
133
+ // 🔌 ROUTE REGISTRATION
134
+ // ═══════════════════════════════════════════════════════════════════════════
135
+
136
+ /**
137
+ * Register TypeOwl routes on your server framework.
138
+ *
139
+ * This function receives:
140
+ * - appInstance: Your server instance (Fastify, Express, Hono, etc.)
141
+ * - typeowl: The TypeOwl handler for processing requests
142
+ * - config: This config object for reference
143
+ *
144
+ * Called when you run: typeowl.mount(app)
145
+ *
146
+ * @example
147
+ * // Fastify
148
+ * registerRoutes: (app, typeowl, config) => {
149
+ * app.get('/__typeowl/*', (req, reply) => { ... });
150
+ * }
151
+ *
152
+ * @example
153
+ * // Express
154
+ * registerRoutes: (app, typeowl, config) => {
155
+ * app.use('/__typeowl', (req, res) => { ... });
156
+ * }
157
+ */
158
+ registerRoutes: (appInstance, typeowl, config) => {
159
+ const app = asFastify(appInstance);
160
+ const basePath = typeowl.getBasePath();
161
+ const guard = config.guard;
162
+
163
+ // Guard check helper
164
+ const checkGuard = (request: { headers: Record<string, string | string[] | undefined>; query: Record<string, string> }) => {
165
+ return typeowl.validateRequest(
166
+ { path: '', headers: request.headers, query: request.query },
167
+ guard
168
+ );
169
+ };
170
+
171
+ // Manifest endpoint: /__typeowl
172
+ app.get(`${basePath}`, async (request, reply) => {
173
+ const auth = checkGuard(request as never);
174
+ if (!auth.valid) return reply.code(401).send({ error: auth.error });
175
+ const response = typeowl.handleRequest(basePath);
176
+ if (response) return reply.type(response.contentType).send(response.body);
177
+ return reply.code(404).send({ error: 'Not found' });
178
+ });
179
+
180
+ // Manifest JSON endpoint: /__typeowl/manifest.json
181
+ app.get(`${basePath}/manifest.json`, async (request, reply) => {
182
+ const auth = checkGuard(request as never);
183
+ if (!auth.valid) return reply.code(401).send({ error: auth.error });
184
+ const response = typeowl.handleRequest(`${basePath}/manifest.json`);
185
+ if (response) return reply.type(response.contentType).send(response.body);
186
+ return reply.code(404).send({ error: 'Not found' });
187
+ });
188
+
189
+ // Type files endpoint: /__typeowl/types/{file}.d.ts
190
+ app.get(`${basePath}/types/:file`, async (request, reply) => {
191
+ const auth = checkGuard(request as never);
192
+ if (!auth.valid) return reply.code(401).send({ error: auth.error });
193
+ const { file } = request.params as { file: string };
194
+ const response = typeowl.handleRequest(`${basePath}/types/${file}`);
195
+ if (response) return reply.type(response.contentType).send(response.body);
196
+ return reply.code(404).send({ error: 'Not found' });
197
+ });
198
+
199
+ console.log(` 🦉 TypeOwl registered at ${basePath}`);
200
+ },
201
+ });