ts-typed-api 0.1.0 → 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.
Files changed (38) hide show
  1. package/README.md +58 -1
  2. package/dist/definition.d.ts +70 -0
  3. package/dist/definition.js +44 -1
  4. package/dist/handler.js +159 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/router.d.ts +7 -1
  7. package/examples/file-upload-example.ts +158 -0
  8. package/package.json +3 -1
  9. package/src/definition.ts +97 -0
  10. package/src/handler.ts +159 -1
  11. package/src/index.ts +1 -0
  12. package/src/router.ts +8 -1
  13. package/dist/apiClient.d.ts +0 -147
  14. package/dist/apiClient.js +0 -206
  15. package/dist/example-server/client.d.ts +0 -2
  16. package/dist/example-server/client.js +0 -19
  17. package/dist/example-server/definitions.d.ts +0 -157
  18. package/dist/example-server/definitions.js +0 -35
  19. package/dist/example-server/server.d.ts +0 -1
  20. package/dist/example-server/server.js +0 -66
  21. package/dist/openapiGenerator.d.ts +0 -2
  22. package/dist/openapiGenerator.js +0 -119
  23. package/dist/router/definition.d.ts +0 -118
  24. package/dist/router/definition.js +0 -80
  25. package/dist/router/router.d.ts +0 -29
  26. package/dist/router/router.js +0 -168
  27. package/dist/src/router/client.d.ts +0 -151
  28. package/dist/src/router/client.js +0 -215
  29. package/dist/src/router/definition.d.ts +0 -121
  30. package/dist/src/router/definition.js +0 -79
  31. package/dist/src/router/handler.d.ts +0 -16
  32. package/dist/src/router/handler.js +0 -170
  33. package/dist/src/router/index.d.ts +0 -5
  34. package/dist/src/router/index.js +0 -22
  35. package/dist/src/router/object-handlers.d.ts +0 -16
  36. package/dist/src/router/object-handlers.js +0 -39
  37. package/dist/src/router/router.d.ts +0 -18
  38. package/dist/src/router/router.js +0 -20
package/README.md CHANGED
@@ -27,7 +27,7 @@ This module is specifically designed to make coding with Large Language Models (
27
27
  ## 📦 Installation
28
28
 
29
29
  ```bash
30
- npm install ts-typed-api
30
+ npm install --save ts-typed-api
31
31
  ```
32
32
 
33
33
 
@@ -144,6 +144,63 @@ async function runClientExample(): Promise<void> {
144
144
 
145
145
  **Now both server and client are type safe and in sync! The moment you change the definition of the API, type system will let you know about potential changes you need to handle (like additional response code or a change request body schema).**
146
146
 
147
+ ### 4. File Upload Example
148
+
149
+ Handle file uploads with type-safe validation:
150
+
151
+ ```typescript
152
+ // Define file upload endpoints
153
+ const FileUploadApiDefinition = CreateApiDefinition({
154
+ prefix: '/api',
155
+ endpoints: {
156
+ files: {
157
+ uploadSingle: {
158
+ path: '/upload/single',
159
+ method: 'POST',
160
+ body: z.object({
161
+ description: z.string().optional(),
162
+ }),
163
+ fileUpload: {
164
+ single: {
165
+ fieldName: 'file',
166
+ maxSize: 5 * 1024 * 1024, // 5MB
167
+ allowedMimeTypes: ['image/jpeg', 'image/png', 'image/gif']
168
+ }
169
+ },
170
+ responses: CreateResponses({
171
+ 200: z.object({
172
+ message: z.string(),
173
+ fileInfo: z.object({
174
+ originalName: z.string(),
175
+ size: z.number(),
176
+ mimetype: z.string()
177
+ })
178
+ })
179
+ })
180
+ }
181
+ }
182
+ }
183
+ });
184
+
185
+ // Implement handler
186
+ RegisterHandlers(app, FileUploadApiDefinition, {
187
+ files: {
188
+ uploadSingle: async (req, res) => {
189
+ const file = req.file as UploadedFile | undefined;
190
+
191
+ res.respond(200, {
192
+ message: 'File uploaded successfully',
193
+ fileInfo: {
194
+ originalName: file!.originalname,
195
+ size: file!.size,
196
+ mimetype: file!.mimetype
197
+ }
198
+ });
199
+ }
200
+ }
201
+ });
202
+ ```
203
+
147
204
  ## 🌟 Features
148
205
 
149
206
  ### Custom HTTP Client Adapters
@@ -73,12 +73,36 @@ type CreateResponsesReturnType<InputSchemas extends Partial<Record<AllowedInputS
73
73
  422: typeof errorUnifiedResponseSchema;
74
74
  };
75
75
  export declare function CreateResponses<TInputMap extends Partial<Record<AllowedInputStatusCode, InputSchemaOrMarker>>>(schemas: TInputMap): CreateResponsesReturnType<TInputMap>;
76
+ export interface FileUploadConfig {
77
+ single?: {
78
+ fieldName: string;
79
+ maxSize?: number;
80
+ allowedMimeTypes?: string[];
81
+ };
82
+ array?: {
83
+ fieldName: string;
84
+ maxCount?: number;
85
+ maxSize?: number;
86
+ allowedMimeTypes?: string[];
87
+ };
88
+ fields?: Array<{
89
+ fieldName: string;
90
+ maxCount?: number;
91
+ maxSize?: number;
92
+ allowedMimeTypes?: string[];
93
+ }>;
94
+ any?: {
95
+ maxSize?: number;
96
+ allowedMimeTypes?: string[];
97
+ };
98
+ }
76
99
  export interface RouteSchema {
77
100
  path: string;
78
101
  method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD';
79
102
  params?: ZodTypeAny;
80
103
  query?: ZodTypeAny;
81
104
  body?: ZodTypeAny;
105
+ fileUpload?: FileUploadConfig;
82
106
  responses: Record<number, ZodTypeAny>;
83
107
  }
84
108
  export type ApiDefinitionSchema = {
@@ -118,4 +142,50 @@ export type ApiClientParams<TDef extends ApiDefinitionSchema, TDomain extends ke
118
142
  export type ApiClientQuery<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteName extends ApiRouteKey<TDef, TDomain>> = TDef['endpoints'][TDomain][TRouteName] extends {
119
143
  query: infer Q extends ZodTypeAny;
120
144
  } ? z.input<Q> : undefined;
145
+ export declare const fileSchema: z.ZodObject<{
146
+ fieldname: z.ZodString;
147
+ originalname: z.ZodString;
148
+ encoding: z.ZodString;
149
+ mimetype: z.ZodString;
150
+ size: z.ZodNumber;
151
+ buffer: z.ZodType<Buffer<ArrayBufferLike>, z.ZodTypeDef, Buffer<ArrayBufferLike>>;
152
+ destination: z.ZodOptional<z.ZodString>;
153
+ filename: z.ZodOptional<z.ZodString>;
154
+ path: z.ZodOptional<z.ZodString>;
155
+ stream: z.ZodOptional<z.ZodAny>;
156
+ }, "strip", z.ZodTypeAny, {
157
+ fieldname: string;
158
+ originalname: string;
159
+ encoding: string;
160
+ mimetype: string;
161
+ size: number;
162
+ buffer: Buffer<ArrayBufferLike>;
163
+ path?: string | undefined;
164
+ destination?: string | undefined;
165
+ filename?: string | undefined;
166
+ stream?: any;
167
+ }, {
168
+ fieldname: string;
169
+ originalname: string;
170
+ encoding: string;
171
+ mimetype: string;
172
+ size: number;
173
+ buffer: Buffer<ArrayBufferLike>;
174
+ path?: string | undefined;
175
+ destination?: string | undefined;
176
+ filename?: string | undefined;
177
+ stream?: any;
178
+ }>;
179
+ export type FileType = z.infer<typeof fileSchema>;
180
+ export declare function createFileValidationSchema(options?: {
181
+ maxSize?: number;
182
+ allowedMimeTypes?: string[];
183
+ required?: boolean;
184
+ }): z.ZodTypeAny;
185
+ export declare function createFilesArrayValidationSchema(options?: {
186
+ maxCount?: number;
187
+ maxSize?: number;
188
+ allowedMimeTypes?: string[];
189
+ required?: boolean;
190
+ }): z.ZodArray<z.ZodTypeAny, "many"> | z.ZodOptional<z.ZodArray<z.ZodTypeAny, "many">>;
121
191
  export {};
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HttpServerErrorCodes = exports.HttpClientErrorCodes = exports.HttpSuccessCodes = exports.TsTypeMarker = void 0;
3
+ exports.fileSchema = exports.HttpServerErrorCodes = exports.HttpClientErrorCodes = exports.HttpSuccessCodes = exports.TsTypeMarker = void 0;
4
4
  exports.CustomResponse = CustomResponse;
5
5
  exports.CreateResponses = CreateResponses;
6
6
  exports.CreateApiDefinition = CreateApiDefinition;
7
+ exports.createFileValidationSchema = createFileValidationSchema;
8
+ exports.createFilesArrayValidationSchema = createFilesArrayValidationSchema;
7
9
  const zod_1 = require("zod");
8
10
  // Marker class for raw TypeScript types
9
11
  class TsTypeMarker {
@@ -61,6 +63,9 @@ function CreateResponses(schemas) {
61
63
  else if (schemaOrMarker instanceof zod_1.ZodType) { // It's a Zod schema
62
64
  builtResult[numericKey] = createSuccessUnifiedResponseSchema(schemaOrMarker);
63
65
  }
66
+ else {
67
+ builtResult[numericKey] = createSuccessUnifiedResponseSchema(schemaOrMarker);
68
+ }
64
69
  // Note: If schemaOrMarker is something else, it would be a type error
65
70
  // based on InputSchemaOrMarker, or this runtime check would skip it.
66
71
  }
@@ -77,3 +82,41 @@ function CreateResponses(schemas) {
77
82
  function CreateApiDefinition(definition) {
78
83
  return definition;
79
84
  }
85
+ // --- File Upload Validation Schemas ---
86
+ // Schema for validating uploaded files
87
+ exports.fileSchema = zod_1.z.object({
88
+ fieldname: zod_1.z.string(),
89
+ originalname: zod_1.z.string(),
90
+ encoding: zod_1.z.string(),
91
+ mimetype: zod_1.z.string(),
92
+ size: zod_1.z.number(),
93
+ buffer: zod_1.z.instanceof(Buffer),
94
+ destination: zod_1.z.string().optional(),
95
+ filename: zod_1.z.string().optional(),
96
+ path: zod_1.z.string().optional(),
97
+ stream: zod_1.z.any().optional(),
98
+ });
99
+ // Helper function to create file validation schema with constraints
100
+ function createFileValidationSchema(options) {
101
+ let schema = exports.fileSchema;
102
+ if (options?.maxSize) {
103
+ schema = schema.refine((file) => file.size <= options.maxSize, { message: `File size must be less than ${options.maxSize} bytes` });
104
+ }
105
+ if (options?.allowedMimeTypes && options.allowedMimeTypes.length > 0) {
106
+ schema = schema.refine((file) => options.allowedMimeTypes.includes(file.mimetype), { message: `File type must be one of: ${options.allowedMimeTypes.join(', ')}` });
107
+ }
108
+ return options?.required === false ? schema.optional() : schema;
109
+ }
110
+ // Helper function to create array of files validation schema
111
+ function createFilesArrayValidationSchema(options) {
112
+ const singleFileSchema = createFileValidationSchema({
113
+ maxSize: options?.maxSize,
114
+ allowedMimeTypes: options?.allowedMimeTypes,
115
+ required: true, // Individual files in array are required
116
+ });
117
+ let schema = zod_1.z.array(singleFileSchema);
118
+ if (options?.maxCount) {
119
+ schema = schema.max(options.maxCount, `Maximum ${options.maxCount} files allowed`);
120
+ }
121
+ return options?.required === false ? schema.optional() : schema;
122
+ }
package/dist/handler.js CHANGED
@@ -1,7 +1,155 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.registerRouteHandlers = registerRouteHandlers;
4
7
  const zod_1 = require("zod");
8
+ const multer_1 = __importDefault(require("multer"));
9
+ // Helper function to create multer middleware based on file upload configuration
10
+ function createFileUploadMiddleware(config) {
11
+ // Default multer configuration
12
+ const storage = multer_1.default.memoryStorage(); // Store files in memory by default
13
+ let multerMiddleware;
14
+ if (config.single) {
15
+ const upload = (0, multer_1.default)({
16
+ storage,
17
+ limits: {
18
+ fileSize: config.single.maxSize || 10 * 1024 * 1024, // Default 10MB
19
+ },
20
+ fileFilter: (req, file, cb) => {
21
+ if (config.single.allowedMimeTypes && !config.single.allowedMimeTypes.includes(file.mimetype)) {
22
+ cb(new Error(`File type ${file.mimetype} not allowed`));
23
+ return;
24
+ }
25
+ cb(null, true);
26
+ }
27
+ });
28
+ multerMiddleware = upload.single(config.single.fieldName);
29
+ }
30
+ else if (config.array) {
31
+ const upload = (0, multer_1.default)({
32
+ storage,
33
+ limits: {
34
+ fileSize: config.array.maxSize || 10 * 1024 * 1024, // Default 10MB per file
35
+ files: config.array.maxCount || 10, // Default max 10 files
36
+ },
37
+ fileFilter: (req, file, cb) => {
38
+ if (config.array.allowedMimeTypes && !config.array.allowedMimeTypes.includes(file.mimetype)) {
39
+ cb(new Error(`File type ${file.mimetype} not allowed`));
40
+ return;
41
+ }
42
+ cb(null, true);
43
+ }
44
+ });
45
+ multerMiddleware = upload.array(config.array.fieldName, config.array.maxCount);
46
+ }
47
+ else if (config.fields) {
48
+ const upload = (0, multer_1.default)({
49
+ storage,
50
+ limits: {
51
+ fileSize: Math.max(...config.fields.map(f => f.maxSize || 10 * 1024 * 1024)), // Use max size from all fields
52
+ },
53
+ fileFilter: (req, file, cb) => {
54
+ const fieldConfig = config.fields.find(f => f.fieldName === file.fieldname);
55
+ if (fieldConfig?.allowedMimeTypes && !fieldConfig.allowedMimeTypes.includes(file.mimetype)) {
56
+ cb(new Error(`File type ${file.mimetype} not allowed for field ${file.fieldname}`));
57
+ return;
58
+ }
59
+ cb(null, true);
60
+ }
61
+ });
62
+ const fields = config.fields.map(f => ({ name: f.fieldName, maxCount: f.maxCount || 1 }));
63
+ multerMiddleware = upload.fields(fields);
64
+ }
65
+ else if (config.any) {
66
+ const upload = (0, multer_1.default)({
67
+ storage,
68
+ limits: {
69
+ fileSize: config.any.maxSize || 10 * 1024 * 1024, // Default 10MB per file
70
+ },
71
+ fileFilter: (req, file, cb) => {
72
+ if (config.any.allowedMimeTypes && !config.any.allowedMimeTypes.includes(file.mimetype)) {
73
+ cb(new Error(`File type ${file.mimetype} not allowed`));
74
+ return;
75
+ }
76
+ cb(null, true);
77
+ }
78
+ });
79
+ multerMiddleware = upload.any();
80
+ }
81
+ else {
82
+ // Fallback - should not reach here if config is valid
83
+ throw new Error('Invalid file upload configuration');
84
+ }
85
+ // Wrap multer middleware with error handling to format errors as 422 JSON responses
86
+ return (req, res, next) => {
87
+ multerMiddleware(req, res, (error) => {
88
+ if (error) {
89
+ // Convert multer errors to UnifiedError format
90
+ const mappedErrors = [];
91
+ if (error instanceof multer_1.default.MulterError) {
92
+ let errorMessage = error.message;
93
+ let fieldName = 'file';
94
+ switch (error.code) {
95
+ case 'LIMIT_FILE_SIZE':
96
+ errorMessage = 'File size exceeds the allowed limit';
97
+ break;
98
+ case 'LIMIT_FILE_COUNT':
99
+ errorMessage = 'Too many files uploaded';
100
+ break;
101
+ case 'LIMIT_UNEXPECTED_FILE':
102
+ errorMessage = `Unexpected field: ${error.field}`;
103
+ fieldName = error.field || 'file';
104
+ break;
105
+ case 'LIMIT_FIELD_KEY':
106
+ errorMessage = 'Field name too long';
107
+ break;
108
+ case 'LIMIT_FIELD_VALUE':
109
+ errorMessage = 'Field value too long';
110
+ break;
111
+ case 'LIMIT_FIELD_COUNT':
112
+ errorMessage = 'Too many fields';
113
+ break;
114
+ case 'LIMIT_PART_COUNT':
115
+ errorMessage = 'Too many parts';
116
+ break;
117
+ default:
118
+ errorMessage = error.message || 'File upload error';
119
+ }
120
+ mappedErrors.push({
121
+ field: fieldName,
122
+ message: errorMessage,
123
+ type: 'body',
124
+ });
125
+ }
126
+ else if (error instanceof Error) {
127
+ // Handle custom errors from fileFilter
128
+ mappedErrors.push({
129
+ field: 'file',
130
+ message: error.message,
131
+ type: 'body',
132
+ });
133
+ }
134
+ else {
135
+ mappedErrors.push({
136
+ field: 'file',
137
+ message: 'File upload error',
138
+ type: 'body',
139
+ });
140
+ }
141
+ // Send 422 response with structured error format
142
+ res.status(422).json({
143
+ data: null,
144
+ error: mappedErrors
145
+ });
146
+ }
147
+ else {
148
+ next();
149
+ }
150
+ });
151
+ };
152
+ }
5
153
  // Register route handlers with Express, now generic over TDef
6
154
  function registerRouteHandlers(app, apiDefinition, // Pass the actual API definition object
7
155
  routeHandlers, // Use the generic handler type
@@ -150,6 +298,17 @@ middlewares) {
150
298
  };
151
299
  // Create middleware wrappers that include endpoint information
152
300
  const middlewareWrappers = [];
301
+ // Add file upload middleware if configured
302
+ if (routeDefinition.fileUpload) {
303
+ try {
304
+ const fileUploadMiddleware = createFileUploadMiddleware(routeDefinition.fileUpload);
305
+ middlewareWrappers.push(fileUploadMiddleware);
306
+ }
307
+ catch (error) {
308
+ console.error(`Error creating file upload middleware for ${currentDomain}.${currentRouteKey}:`, error);
309
+ return; // Skip this route if file upload middleware creation fails
310
+ }
311
+ }
153
312
  if (middlewares && middlewares.length > 0) {
154
313
  middlewares.forEach(middleware => {
155
314
  const wrappedMiddleware = async (req, res, next) => {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { ApiClient, FetchHttpClientAdapter } from './client';
2
2
  export { CreateApiDefinition, CreateResponses } from './definition';
3
3
  export { RegisterHandlers, EndpointMiddleware } from './object-handlers';
4
+ export { File as UploadedFile } from './router';
4
5
  export { z as ZodSchema } from 'zod';
package/dist/router.d.ts CHANGED
@@ -1,8 +1,14 @@
1
1
  import express from "express";
2
2
  import { ApiDefinitionSchema, // Changed from ApiDefinition
3
3
  ApiBody, ApiParams, ApiQuery, InferDataFromUnifiedResponse } from './definition';
4
+ export type File = Express.Multer.File;
4
5
  export type TypedRequest<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteKey extends keyof TDef['endpoints'][TDomain], // Using direct keyof for simplicity here
5
- P extends ApiParams<TDef, TDomain, TRouteKey> = ApiParams<TDef, TDomain, TRouteKey>, ReqBody extends ApiBody<TDef, TDomain, TRouteKey> = ApiBody<TDef, TDomain, TRouteKey>, Q extends ApiQuery<TDef, TDomain, TRouteKey> = ApiQuery<TDef, TDomain, TRouteKey>, L extends Record<string, any> = Record<string, any>> = express.Request<P, any, ReqBody, Q, L>;
6
+ P extends ApiParams<TDef, TDomain, TRouteKey> = ApiParams<TDef, TDomain, TRouteKey>, ReqBody extends ApiBody<TDef, TDomain, TRouteKey> = ApiBody<TDef, TDomain, TRouteKey>, Q extends ApiQuery<TDef, TDomain, TRouteKey> = ApiQuery<TDef, TDomain, TRouteKey>, L extends Record<string, any> = Record<string, any>> = express.Request<P, any, ReqBody, Q, L> & {
7
+ file?: File;
8
+ files?: File[] | {
9
+ [fieldname: string]: File[];
10
+ };
11
+ };
6
12
  type ResponseDataForStatus<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteName extends keyof TDef['endpoints'][TDomain], TStatus extends keyof TDef['endpoints'][TDomain][TRouteName]['responses'] & number> = InferDataFromUnifiedResponse<TDef['endpoints'][TDomain][TRouteName]['responses'][TStatus]>;
7
13
  type RespondFunction<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteName extends keyof TDef['endpoints'][TDomain]> = <TStatusLocal extends keyof TDef['endpoints'][TDomain][TRouteName]['responses'] & number>(status: TStatusLocal, data: ResponseDataForStatus<TDef, TDomain, TRouteName, TStatusLocal>) => void;
8
14
  export interface TypedResponse<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteName extends keyof TDef['endpoints'][TDomain], L extends Record<string, any> = Record<string, any>> extends express.Response<any, L> {
@@ -0,0 +1,158 @@
1
+ import express from 'express';
2
+ import { z } from 'zod';
3
+ import {
4
+ CreateApiDefinition,
5
+ CreateResponses,
6
+ RegisterHandlers,
7
+ UploadedFile
8
+ } from '../src';
9
+
10
+ // Define API with file upload endpoints
11
+ const FileUploadApiDefinition = CreateApiDefinition({
12
+ prefix: '/api',
13
+ endpoints: {
14
+ files: {
15
+ // Single file upload
16
+ uploadSingle: {
17
+ path: '/upload/single',
18
+ method: 'POST',
19
+ body: z.object({
20
+ description: z.string().optional(),
21
+ }),
22
+ fileUpload: {
23
+ single: {
24
+ fieldName: 'file',
25
+ maxSize: 5 * 1024 * 1024, // 5MB
26
+ allowedMimeTypes: ['image/jpeg', 'image/png', 'image/gif']
27
+ }
28
+ },
29
+ responses: CreateResponses({
30
+ 200: z.object({
31
+ message: z.string(),
32
+ fileInfo: z.object({
33
+ originalName: z.string(),
34
+ size: z.number(),
35
+ mimetype: z.string()
36
+ })
37
+ })
38
+ })
39
+ },
40
+
41
+ // Multiple files upload (array)
42
+ uploadMultiple: {
43
+ path: '/upload/multiple',
44
+ method: 'POST',
45
+ body: z.object({
46
+ category: z.string(),
47
+ }),
48
+ fileUpload: {
49
+ array: {
50
+ fieldName: 'files',
51
+ maxCount: 5,
52
+ maxSize: 2 * 1024 * 1024, // 2MB per file
53
+ allowedMimeTypes: ['image/jpeg', 'image/png']
54
+ }
55
+ },
56
+ responses: CreateResponses({
57
+ 200: z.object({
58
+ message: z.string(),
59
+ uploadedFiles: z.array(z.object({
60
+ originalName: z.string(),
61
+ size: z.number(),
62
+ mimetype: z.string()
63
+ }))
64
+ })
65
+ })
66
+ },
67
+ }
68
+ }
69
+ });
70
+
71
+ // Create Express app
72
+ const app = express();
73
+ const port = 3002;
74
+
75
+ // Note: Don't use express.json() for file upload routes as it conflicts with multipart parsing
76
+ // Only use it for non-file routes or apply it selectively
77
+
78
+ // Register handlers
79
+ RegisterHandlers(app, FileUploadApiDefinition, {
80
+ files: {
81
+ uploadSingle: async (req, res) => {
82
+ // req.file contains the uploaded file
83
+ // req.body contains the form data
84
+
85
+ const file = req.file as UploadedFile | undefined;
86
+
87
+ try {
88
+
89
+ // Process the file here (save to disk, upload to cloud, etc.)
90
+ console.log('Uploaded file:', {
91
+ name: file!.originalname,
92
+ size: file!.size,
93
+ type: file!.mimetype
94
+ });
95
+
96
+ res.respond(200, {
97
+ message: 'File uploaded successfully',
98
+ fileInfo: {
99
+ originalName: file!.originalname,
100
+ size: file!.size,
101
+ mimetype: file!.mimetype
102
+ }
103
+ });
104
+ } catch (error) {
105
+ console.error('File validation error:', error);
106
+
107
+ }
108
+ },
109
+
110
+ uploadMultiple: async (req, res) => {
111
+ // req.files contains array of uploaded files
112
+ // req.body contains the form data
113
+ const files = req.files as UploadedFile[] | undefined;
114
+
115
+ try {
116
+ // Process the files here
117
+ const uploadedFiles = files!.map(file => ({
118
+ originalName: file.originalname,
119
+ size: file.size,
120
+ mimetype: file.mimetype
121
+ }));
122
+ console.log('Uploaded files:', uploadedFiles, req.body.category);
123
+
124
+ res.respond(200, {
125
+ message: `${uploadedFiles.length} files uploaded successfully`,
126
+ uploadedFiles
127
+ });
128
+
129
+ } catch (error) {
130
+ console.error('Files validation error:', error);
131
+
132
+ }
133
+ },
134
+ }
135
+ });
136
+
137
+ app.listen(port, () => {
138
+ console.log(`File upload server listening at http://localhost:${port}`);
139
+ console.log('\nExample curl commands:');
140
+ console.log('\n1. Single file upload:');
141
+ console.log(`curl -X POST http://localhost:${port}/api/upload/single \\`);
142
+ console.log(` -F "file=@/path/to/image.jpg" \\`);
143
+ console.log(` -F "description=My uploaded image"`);
144
+
145
+ console.log('\n2. Multiple files upload:');
146
+ console.log(`curl -X POST http://localhost:${port}/api/upload/multiple \\`);
147
+ console.log(` -F "files=@/path/to/image1.jpg" \\`);
148
+ console.log(` -F "files=@/path/to/image2.png" \\`);
149
+ console.log(` -F "category=photos"`);
150
+
151
+ console.log('\n3. Mixed fields upload:');
152
+ console.log(`curl -X POST http://localhost:${port}/api/upload/mixed \\`);
153
+ console.log(` -F "avatar=@/path/to/avatar.jpg" \\`);
154
+ console.log(` -F "documents=@/path/to/doc1.pdf" \\`);
155
+ console.log(` -F "documents=@/path/to/doc2.txt" \\`);
156
+ console.log(` -F "title=My Upload" \\`);
157
+ console.log(` -F "tags=tag1,tag2"`);
158
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-typed-api",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A lightweight, type-safe RPC library for TypeScript with Zod validation",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,8 +26,10 @@
26
26
  "license": "ISC",
27
27
  "dependencies": {
28
28
  "@types/express": "^5.0.3",
29
+ "@types/multer": "^1.4.13",
29
30
  "@types/node": "^24.0.3",
30
31
  "express": "^5.1.0",
32
+ "multer": "^2.0.1",
31
33
  "zod": "^3.22.4"
32
34
  },
33
35
  "devDependencies": {