├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── README.md ├── lib ├── src │ ├── config.js │ ├── createModuleShim │ │ ├── createImportDeclarations.ts │ │ ├── createShim.ts │ │ └── index.ts │ ├── createProgram │ │ ├── createConfigFileHost.ts │ │ └── index.ts │ ├── createRootShim │ │ ├── createApiHandler.ts │ │ ├── createImportDeclarations.ts │ │ ├── createInterfacePlaceholders.ts │ │ ├── createReqBody.ts │ │ ├── createReqQuery.ts │ │ ├── createResBody.ts │ │ └── index.ts │ ├── emitFile.ts │ ├── emitModulesShim.ts │ ├── emitRootShim.ts │ ├── index.ts │ ├── mapFileInfo.ts │ ├── printList.ts │ └── types.ts └── tsconfig.json ├── next-env.d.ts ├── next.config.js ├── nodemon.json ├── package-lock.json ├── package.json ├── src ├── components │ └── Layout.tsx ├── hooks │ └── useApiData.ts ├── models │ ├── articles.ts │ └── users.ts ├── pages │ ├── _app.tsx │ ├── api │ │ ├── articles │ │ │ ├── [id].ts │ │ │ └── index.ts │ │ ├── greet.ts │ │ └── users │ │ │ ├── [id].ts │ │ │ └── index.ts │ ├── articles │ │ ├── [id].tsx │ │ └── index.tsx │ ├── index.tsx │ └── users │ │ ├── [id].tsx │ │ └── index.tsx ├── public │ ├── favicon.ico │ └── vercel.svg ├── types │ └── api.d.ts └── utils │ └── fetcher.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | .vscode 36 | 37 | # generated 38 | src/types/pages* 39 | 40 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # src/types/pages* 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nextjs-typesafe-api-routes 2 | 3 | This is a Type-Safe solution for Next.js API Routes.There was a risk of making some mistakes between API Routes and Client.By using this solution, it is possible to build a type-safe environment by linking both type inferences. 4 | 5 | ## 1.Define API Routes Handlers 6 | 7 | First, define API Routes handler with `ApiHandler`.As defined below, it is necessary to divide the handler for each request method. 8 | 9 | ```typescript 10 | import type { ApiHandler } from "@/types/pages/api"; 11 | 12 | export type GetHandler = ApiHandler<{ name: string }, {}, { message: string }>; 13 | const getHandler: GetHandler = (req, res) => { 14 | if (!req.query.name) { 15 | res 16 | .status(400) 17 | .json({ error: { httpStatus: 400, message: "Invalid Request" } }); 18 | return; 19 | } 20 | res.status(200).json({ data: { message: `hello ${req.query.name}` } }); 21 | }; 22 | 23 | const handler: ApiHandler = (req, res) => { 24 | switch (req.method) { 25 | case "GET": 26 | getHandler(req, res); 27 | break; 28 | default: 29 | res 30 | .status(405) 31 | .json({ error: { httpStatus: 405, message: "Method Not Allowed" } }); 32 | } 33 | }; 34 | export default handler; 35 | ``` 36 | 37 | Generics for `ApiHandler`, expect three Generics `ResBody, ReqQuery, ReqBody` in order. 38 | 39 | ## 2.Generate Type Definitions 40 | 41 | Run below npm scripts, then generate api types into `src/types/pages/api/**/*`. 42 | 43 | ```shell 44 | $ npm run gen:apitype 45 | ``` 46 | 47 | ## 3.Check Type Inference in Client 48 | 49 | `useApiData` provides type inference from the specified string such as `"/api/greet"`. 50 | 51 | ```typescript 52 | const { data } = useApiData("/api/greet", { query: { name: "user" } }); 53 | ``` 54 | -------------------------------------------------------------------------------- /lib/src/config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | // ______________________________________________________ 3 | // 4 | module.exports = { 5 | moduleNameSpaece: "@/types/pages/api", 6 | baseDir: path.resolve("."), 7 | pagesDir: path.resolve("src/pages"), 8 | distDir: path.resolve("src/types/pages/api"), 9 | }; 10 | -------------------------------------------------------------------------------- /lib/src/createModuleShim/createImportDeclarations.ts: -------------------------------------------------------------------------------- 1 | import { factory } from "typescript"; 2 | // ______________________________________________________ 3 | // 4 | // OUTPUT: 5 | // import type { ${method[0]}Handler, ${method[1]}Handler } from "${importPath}"; 6 | // 7 | export const createImportDeclarations = ( 8 | methods: string[], 9 | importPath: string 10 | ) => 11 | factory.createImportDeclaration( 12 | undefined, 13 | undefined, 14 | factory.createImportClause( 15 | true, 16 | undefined, 17 | factory.createNamedImports( 18 | methods.map((method) => 19 | factory.createImportSpecifier( 20 | false, 21 | undefined, 22 | factory.createIdentifier(method + "Handler") 23 | ) 24 | ) 25 | ) 26 | ), 27 | factory.createStringLiteral(importPath), 28 | undefined 29 | ); 30 | -------------------------------------------------------------------------------- /lib/src/createModuleShim/createShim.ts: -------------------------------------------------------------------------------- 1 | import { factory } from "typescript"; 2 | // ______________________________________________________ 3 | // 4 | // OUTPUT: 5 | // interface ${method}${shimName} { 6 | // "${apiPath}": ${shimName}<${method}Handler>; 7 | // } 8 | // 9 | export const createShim = (method: string, apiPath: string, shimName: string) => 10 | factory.createInterfaceDeclaration( 11 | undefined, 12 | undefined, 13 | factory.createIdentifier(method + shimName), 14 | undefined, 15 | undefined, 16 | [ 17 | factory.createPropertySignature( 18 | undefined, 19 | factory.createStringLiteral(apiPath), 20 | undefined, 21 | factory.createTypeReferenceNode(factory.createIdentifier(shimName), [ 22 | factory.createTypeReferenceNode( 23 | factory.createIdentifier(method + "Handler"), 24 | undefined 25 | ), 26 | ]) 27 | ), 28 | ] 29 | ); 30 | -------------------------------------------------------------------------------- /lib/src/createModuleShim/index.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { factory } from "typescript"; 3 | import { createImportDeclarations } from "./createImportDeclarations"; 4 | import { createShim } from "./createShim"; 5 | // ______________________________________________________ 6 | // 7 | export const createModuleShim = ({ 8 | methods, 9 | apiPath, 10 | importPath, 11 | moduleNameSpaece, 12 | }: { 13 | methods: string[]; 14 | apiPath: string; 15 | importPath: string; 16 | moduleNameSpaece: string; 17 | }) => [ 18 | createImportDeclarations(methods, importPath), 19 | factory.createModuleDeclaration( 20 | undefined, 21 | [factory.createModifier(ts.SyntaxKind.DeclareKeyword)], 22 | factory.createStringLiteral(moduleNameSpaece), 23 | factory.createModuleBlock([ 24 | ...methods.map((method) => createShim(method, apiPath, "ReqQuery")), 25 | ...methods.map((method) => createShim(method, apiPath, "ReqBody")), 26 | ...methods.map((method) => createShim(method, apiPath, "ResBody")), 27 | ]) 28 | ), 29 | ]; 30 | -------------------------------------------------------------------------------- /lib/src/createProgram/createConfigFileHost.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | // ______________________________________________________ 3 | // 4 | export const createConfigFileHost = (): ts.ParseConfigFileHost => ({ 5 | useCaseSensitiveFileNames: false, 6 | readDirectory: ts.sys.readDirectory, 7 | fileExists: ts.sys.fileExists, 8 | readFile: ts.sys.readFile, 9 | getCurrentDirectory: ts.sys.getCurrentDirectory, 10 | onUnRecoverableConfigFileDiagnostic(diagnostic: ts.Diagnostic) { 11 | throw new Error( 12 | ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n") 13 | ); 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /lib/src/createProgram/index.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { createConfigFileHost } from "./createConfigFileHost"; 3 | // ______________________________________________________ 4 | // 5 | export const createProgram = ( 6 | searchPath: string, 7 | configName = "tsconfig.json" 8 | ): ts.Program => { 9 | const configPath = ts.findConfigFile( 10 | searchPath, 11 | ts.sys.fileExists, 12 | configName 13 | ); 14 | if (!configPath) { 15 | throw new Error("Could not find a valid 'tsconfig.json'."); 16 | } 17 | const parsedCommandLine = ts.getParsedCommandLineOfConfigFile( 18 | configPath, 19 | {}, 20 | createConfigFileHost() 21 | ); 22 | if (!parsedCommandLine) { 23 | throw new Error("invalid parsedCommandLine."); 24 | } 25 | if (parsedCommandLine.errors.length) { 26 | throw new Error("parsedCommandLine has errors."); 27 | } 28 | return ts.createProgram({ 29 | rootNames: parsedCommandLine.fileNames, 30 | options: parsedCommandLine.options, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/src/createRootShim/createApiHandler.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { factory } from "typescript"; 3 | // ______________________________________________________ 4 | // 5 | // OUTPUT: 6 | // export type ApiHandler = ( 7 | // req: Omit & { 8 | // query: Partial; 9 | // body?: B; 10 | // }, 11 | // res: NextApiResponse | Error> 12 | // ) => void | Promise; 13 | // 14 | export const createApiHandler = () => 15 | factory.createTypeAliasDeclaration( 16 | undefined, 17 | [factory.createModifier(ts.SyntaxKind.ExportKeyword)], 18 | factory.createIdentifier("ApiHandler"), 19 | [ 20 | factory.createTypeParameterDeclaration( 21 | factory.createIdentifier("Q"), 22 | undefined, 23 | factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) 24 | ), 25 | factory.createTypeParameterDeclaration( 26 | factory.createIdentifier("B"), 27 | undefined, 28 | factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) 29 | ), 30 | factory.createTypeParameterDeclaration( 31 | factory.createIdentifier("R"), 32 | undefined, 33 | factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) 34 | ), 35 | ], 36 | factory.createFunctionTypeNode( 37 | undefined, 38 | [ 39 | factory.createParameterDeclaration( 40 | undefined, 41 | undefined, 42 | undefined, 43 | factory.createIdentifier("req"), 44 | undefined, 45 | factory.createIntersectionTypeNode([ 46 | factory.createTypeReferenceNode(factory.createIdentifier("Omit"), [ 47 | factory.createTypeReferenceNode( 48 | factory.createIdentifier("NextApiRequest"), 49 | undefined 50 | ), 51 | factory.createUnionTypeNode([ 52 | factory.createLiteralTypeNode( 53 | factory.createStringLiteral("body") 54 | ), 55 | factory.createLiteralTypeNode( 56 | factory.createStringLiteral("query") 57 | ), 58 | ]), 59 | ]), 60 | factory.createTypeLiteralNode([ 61 | factory.createPropertySignature( 62 | undefined, 63 | factory.createIdentifier("query"), 64 | undefined, 65 | factory.createTypeReferenceNode( 66 | factory.createIdentifier("Partial"), 67 | [ 68 | factory.createTypeReferenceNode( 69 | factory.createIdentifier("Q"), 70 | undefined 71 | ), 72 | ] 73 | ) 74 | ), 75 | factory.createPropertySignature( 76 | undefined, 77 | factory.createIdentifier("body"), 78 | factory.createToken(ts.SyntaxKind.QuestionToken), 79 | factory.createTypeReferenceNode( 80 | factory.createIdentifier("B"), 81 | undefined 82 | ) 83 | ), 84 | ]), 85 | ]), 86 | undefined 87 | ), 88 | factory.createParameterDeclaration( 89 | undefined, 90 | undefined, 91 | undefined, 92 | factory.createIdentifier("res"), 93 | undefined, 94 | factory.createTypeReferenceNode( 95 | factory.createIdentifier("NextApiResponse"), 96 | [ 97 | factory.createUnionTypeNode([ 98 | factory.createTypeReferenceNode( 99 | factory.createIdentifier("Data"), 100 | [ 101 | factory.createTypeReferenceNode( 102 | factory.createIdentifier("R"), 103 | undefined 104 | ), 105 | ] 106 | ), 107 | factory.createTypeReferenceNode( 108 | factory.createIdentifier("Error"), 109 | undefined 110 | ), 111 | ]), 112 | ] 113 | ), 114 | undefined 115 | ), 116 | ], 117 | factory.createUnionTypeNode([ 118 | factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword), 119 | factory.createTypeReferenceNode(factory.createIdentifier("Promise"), [ 120 | factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword), 121 | ]), 122 | ]) 123 | ) 124 | ); 125 | -------------------------------------------------------------------------------- /lib/src/createRootShim/createImportDeclarations.ts: -------------------------------------------------------------------------------- 1 | import { factory } from "typescript"; 2 | // ______________________________________________________ 3 | // 4 | // OUTPUT: 5 | // import type { NextApiRequest, NextApiResponse } from "next"; 6 | // 7 | export const createImportDeclarations = () => 8 | factory.createImportDeclaration( 9 | undefined, 10 | undefined, 11 | factory.createImportClause( 12 | true, 13 | undefined, 14 | factory.createNamedImports([ 15 | factory.createImportSpecifier( 16 | false, 17 | undefined, 18 | factory.createIdentifier("NextApiRequest") 19 | ), 20 | factory.createImportSpecifier( 21 | false, 22 | undefined, 23 | factory.createIdentifier("NextApiResponse") 24 | ), 25 | ]) 26 | ), 27 | factory.createStringLiteral("next"), 28 | undefined 29 | ); 30 | -------------------------------------------------------------------------------- /lib/src/createRootShim/createInterfacePlaceholders.ts: -------------------------------------------------------------------------------- 1 | import { factory } from "typescript"; 2 | // ______________________________________________________ 3 | // 4 | // OUTPUT: 5 | // interface GetResBody {} 6 | // interface GetReqQuery {} 7 | // interface GetReqBody {} 8 | // interface PostResBody {} 9 | // interface PostReqQuery {} 10 | // interface PostReqBody {} 11 | // interface PutResBody {} 12 | // interface PutReqQuery {} 13 | // interface PutReqBody {} 14 | // interface PatchResBody {} 15 | // interface PatchReqQuery {} 16 | // interface PatchReqBody {} 17 | // interface DeleteResBody {} 18 | // interface DeleteReqQuery {} 19 | // interface DeleteReqBody {} 20 | // 21 | const interfacePlaceholders = [ 22 | "GetResBody", 23 | "GetReqQuery", 24 | "GetReqBody", 25 | "PostResBody", 26 | "PostReqQuery", 27 | "PostReqBody", 28 | "PutResBody", 29 | "PutReqQuery", 30 | "PutReqBody", 31 | "PatchResBody", 32 | "PatchReqQuery", 33 | "PatchReqBody", 34 | "DeleteResBody", 35 | "DeleteReqQuery", 36 | "DeleteReqBody", 37 | ]; 38 | export const createInterfacePlaceholders = () => 39 | interfacePlaceholders.map((identifier) => 40 | factory.createInterfaceDeclaration( 41 | undefined, 42 | undefined, 43 | factory.createIdentifier(identifier), 44 | undefined, 45 | undefined, 46 | [] 47 | ) 48 | ); 49 | -------------------------------------------------------------------------------- /lib/src/createRootShim/createReqBody.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { factory } from "typescript"; 3 | // ______________________________________________________ 4 | // 5 | // OUTPUT: 6 | // type ReqBody = T extends ApiHandler ? I : never; 7 | // 8 | export const createReqBody = () => 9 | factory.createTypeAliasDeclaration( 10 | undefined, 11 | undefined, 12 | factory.createIdentifier("ReqBody"), 13 | [ 14 | factory.createTypeParameterDeclaration( 15 | factory.createIdentifier("T"), 16 | undefined, 17 | undefined 18 | ), 19 | ], 20 | factory.createConditionalTypeNode( 21 | factory.createTypeReferenceNode(factory.createIdentifier("T"), undefined), 22 | factory.createTypeReferenceNode(factory.createIdentifier("ApiHandler"), [ 23 | factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), 24 | factory.createInferTypeNode( 25 | factory.createTypeParameterDeclaration( 26 | factory.createIdentifier("I"), 27 | undefined, 28 | undefined 29 | ) 30 | ), 31 | factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), 32 | ]), 33 | factory.createTypeReferenceNode(factory.createIdentifier("I"), undefined), 34 | factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword) 35 | ) 36 | ); 37 | -------------------------------------------------------------------------------- /lib/src/createRootShim/createReqQuery.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { factory } from "typescript"; 3 | // ______________________________________________________ 4 | // 5 | // OUTPUT: 6 | // type ReqQuery = T extends ApiHandler ? I : never; 7 | // 8 | export const createReqQuery = () => 9 | factory.createTypeAliasDeclaration( 10 | undefined, 11 | undefined, 12 | factory.createIdentifier("ReqQuery"), 13 | [ 14 | factory.createTypeParameterDeclaration( 15 | factory.createIdentifier("T"), 16 | undefined, 17 | undefined 18 | ), 19 | ], 20 | factory.createConditionalTypeNode( 21 | factory.createTypeReferenceNode(factory.createIdentifier("T"), undefined), 22 | factory.createTypeReferenceNode(factory.createIdentifier("ApiHandler"), [ 23 | factory.createInferTypeNode( 24 | factory.createTypeParameterDeclaration( 25 | factory.createIdentifier("I"), 26 | undefined, 27 | undefined 28 | ) 29 | ), 30 | factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), 31 | factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), 32 | ]), 33 | factory.createTypeReferenceNode(factory.createIdentifier("I"), undefined), 34 | factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword) 35 | ) 36 | ); 37 | -------------------------------------------------------------------------------- /lib/src/createRootShim/createResBody.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { factory } from "typescript"; 3 | // ______________________________________________________ 4 | // 5 | // OUTPUT: 6 | // type ResBody = T extends ApiHandler ? I : never; 7 | // 8 | export const createResBody = () => 9 | factory.createTypeAliasDeclaration( 10 | undefined, 11 | undefined, 12 | factory.createIdentifier("ResBody"), 13 | [ 14 | factory.createTypeParameterDeclaration( 15 | factory.createIdentifier("T"), 16 | undefined, 17 | undefined 18 | ), 19 | ], 20 | factory.createConditionalTypeNode( 21 | factory.createTypeReferenceNode(factory.createIdentifier("T"), undefined), 22 | factory.createTypeReferenceNode(factory.createIdentifier("ApiHandler"), [ 23 | factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), 24 | factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), 25 | factory.createInferTypeNode( 26 | factory.createTypeParameterDeclaration( 27 | factory.createIdentifier("I"), 28 | undefined, 29 | undefined 30 | ) 31 | ), 32 | ]), 33 | factory.createTypeReferenceNode(factory.createIdentifier("I"), undefined), 34 | factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword) 35 | ) 36 | ); 37 | -------------------------------------------------------------------------------- /lib/src/createRootShim/index.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { factory } from "typescript"; 3 | import { createApiHandler } from "./createApiHandler"; 4 | import { createImportDeclarations } from "./createImportDeclarations"; 5 | import { createInterfacePlaceholders } from "./createInterfacePlaceholders"; 6 | import { createReqBody } from "./createReqBody"; 7 | import { createReqQuery } from "./createReqQuery"; 8 | import { createResBody } from "./createResBody"; 9 | // ______________________________________________________ 10 | // 11 | export const createRootShim = (moduleNameSpaece: string) => [ 12 | createImportDeclarations(), 13 | factory.createModuleDeclaration( 14 | undefined, 15 | [factory.createModifier(ts.SyntaxKind.DeclareKeyword)], 16 | factory.createStringLiteral(moduleNameSpaece), 17 | factory.createModuleBlock([ 18 | createApiHandler(), 19 | createReqQuery(), 20 | createReqBody(), 21 | createResBody(), 22 | ...createInterfacePlaceholders(), 23 | ]) 24 | ), 25 | ]; 26 | -------------------------------------------------------------------------------- /lib/src/emitFile.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs-extra"; 2 | // ______________________________________________________ 3 | // 4 | export const emitFile = ( 5 | distDir: string, 6 | fileName: string, 7 | fileBody: string 8 | ) => { 9 | if (!fs.existsSync(distDir)) { 10 | fs.mkdirsSync(distDir); 11 | } 12 | fs.writeFileSync(fileName, fileBody); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/src/emitModulesShim.ts: -------------------------------------------------------------------------------- 1 | import { createModuleShim } from "./createModuleShim"; 2 | import { emitFile } from "./emitFile"; 3 | import { printList } from "./printList"; 4 | import type { FileInfo } from "./types"; 5 | // ______________________________________________________ 6 | // 7 | export function emitModulesShim( 8 | fileInfos: FileInfo[], 9 | moduleNameSpaece: string 10 | ) { 11 | fileInfos.map((info) => { 12 | emitFile( 13 | info.distDir, 14 | info.distPath, 15 | printList( 16 | createModuleShim({ 17 | methods: info.methodTypes, 18 | apiPath: info.apiPath, 19 | importPath: info.importPath, 20 | moduleNameSpaece, 21 | }) 22 | ) 23 | ); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/emitRootShim.ts: -------------------------------------------------------------------------------- 1 | import { createRootShim } from "./createRootShim"; 2 | import { emitFile } from "./emitFile"; 3 | import { printList } from "./printList"; 4 | // ______________________________________________________ 5 | // 6 | export function emitRootShim(disrDir: string, moduleNameSpaece: string) { 7 | emitFile( 8 | disrDir, 9 | disrDir + "/index.d.ts", 10 | printList(createRootShim(moduleNameSpaece)) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/index.ts: -------------------------------------------------------------------------------- 1 | import config from "./config"; 2 | import { createProgram } from "./createProgram"; 3 | import { emitModulesShim } from "./emitModulesShim"; 4 | import { emitRootShim } from "./emitRootShim"; 5 | import { mapFileInfo } from "./mapFileInfo"; 6 | import type { Config } from "./types"; 7 | // ______________________________________________________ 8 | // 9 | function main({ baseDir, pagesDir, distDir, moduleNameSpaece }: Config) { 10 | const apiDir = pagesDir + "/api"; 11 | const program = createProgram(baseDir); 12 | const fileInfos = program 13 | .getRootFileNames() 14 | .filter((fileName) => fileName.match(apiDir)) 15 | .map(mapFileInfo(apiDir, distDir, pagesDir, program)); 16 | 17 | emitRootShim(distDir, moduleNameSpaece); 18 | emitModulesShim(fileInfos, moduleNameSpaece); 19 | } 20 | 21 | main(config); 22 | -------------------------------------------------------------------------------- /lib/src/mapFileInfo.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import type { FileInfo } from "./types"; 3 | // ______________________________________________________ 4 | // 5 | const targetAliases = [ 6 | "GetHandler", 7 | "PostHandler", 8 | "PutHandler", 9 | "PatchHandler", 10 | "DeleteHandler", 11 | ]; 12 | function getMethodTypes(sourceFile?: ts.SourceFile) { 13 | const buf: string[] = []; 14 | if (sourceFile) { 15 | sourceFile.forEachChild((node) => { 16 | if (ts.isTypeAliasDeclaration(node)) { 17 | const name = node.name.escapedText.toString(); 18 | if (targetAliases.includes(name)) { 19 | buf.push(name.replace("Handler", "")); 20 | } 21 | } 22 | }); 23 | } 24 | return buf; 25 | } 26 | // ______________________________________________________ 27 | // 28 | export function mapFileInfo( 29 | src: string, 30 | dist: string, 31 | pagesDir: string, 32 | program: ts.Program 33 | ) { 34 | return (filePath: string): FileInfo => { 35 | const srcPath = filePath; 36 | const distArr = filePath.replace(src, dist).split("/"); 37 | const distFileName = distArr[distArr.length - 1].replace(".ts", ".d.ts"); 38 | const distDir = distArr.splice(0, distArr.length - 1).join("/"); 39 | const distPath = `${distDir}/${distFileName}`; 40 | const sourceFile = program.getSourceFile(srcPath); 41 | const importPath = filePath.replace(".ts", ""); 42 | const apiPath = filePath 43 | .replace(pagesDir, "") 44 | .replace("/index", "") 45 | .slice(0, -3); 46 | const methodTypes = getMethodTypes(sourceFile); 47 | return { 48 | srcPath, 49 | distPath, 50 | distFileName, 51 | distDir, 52 | filePath, 53 | methodTypes, 54 | importPath, 55 | apiPath, 56 | }; 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/printList.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | // ______________________________________________________ 3 | // 4 | const printer = ts.createPrinter(); 5 | 6 | export const printList = (elements?: readonly ts.Node[]) => 7 | printer.printList( 8 | ts.ListFormat.MultiLine, 9 | ts.factory.createNodeArray(elements), 10 | ts.createSourceFile("", "", ts.ScriptTarget.ES2015) 11 | ); 12 | -------------------------------------------------------------------------------- /lib/src/types.ts: -------------------------------------------------------------------------------- 1 | export type FileInfo = { 2 | srcPath: string; 3 | distPath: string; 4 | distFileName: string; 5 | distDir: string; 6 | filePath: string; 7 | methodTypes: string[]; 8 | importPath: string; 9 | apiPath: string; 10 | }; 11 | 12 | export type Config = { 13 | baseDir: string; 14 | distDir: string; 15 | pagesDir: string; 16 | moduleNameSpaece: string; 17 | }; 18 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "allowJs": true, 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "rootDir": "src", 10 | "outDir": "./dist" 11 | }, 12 | "include": ["./src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["lib"], 3 | "ext": "ts", 4 | "exec": "ts-node ./lib/src/index.ts" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "typecheck": "tsc --noEmit", 11 | "format": "prettier --write \"src/**/*.{ts,tsx}\" \"lib/**/*.{ts,tsx}\"", 12 | "postinstall": "npm run gen:apitype", 13 | "gen:apitype": "npm run gen:apitype:clean && npm run gen:apitype:build", 14 | "gen:apitype:build": "ts-node ./lib/src/index.ts", 15 | "gen:apitype:clean": "rimraf ./src/types/pages/api", 16 | "gen:apitype:watch": "nodemon" 17 | }, 18 | "dependencies": { 19 | "fs-extra": "^10.0.0", 20 | "next": "12.0.1", 21 | "query-string": "^7.0.1", 22 | "react": "17.0.2", 23 | "react-dom": "17.0.2", 24 | "swr": "^1.0.1" 25 | }, 26 | "devDependencies": { 27 | "@types/fs-extra": "^9.0.13", 28 | "@types/react": "^17.0.37", 29 | "eslint": "7.32.0", 30 | "eslint-config-next": "11.1.0", 31 | "nodemon": "^2.0.15", 32 | "prettier": "^2.5.0", 33 | "prettier-plugin-organize-imports": "^2.3.4", 34 | "rimraf": "^3.0.2", 35 | "ts-node": "^10.4.0", 36 | "typescript": "^4.5.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | // _____________________________________________________________________________ 4 | // 5 | const Layout: React.FC = ({ children }) => { 6 | return ( 7 |
8 | 27 |
28 |
{children}
29 |
30 | ); 31 | }; 32 | 33 | export default Layout; 34 | -------------------------------------------------------------------------------- /src/hooks/useApiData.ts: -------------------------------------------------------------------------------- 1 | import type { Error, GetReqQuery, GetResBody } from "@/types/pages/api"; 2 | import qs from "query-string"; 3 | import useSWR, { SWRConfiguration } from "swr"; 4 | // _____________________________________________________________________________ 5 | // 6 | export function useApiData< 7 | T extends keyof GetResBody, 8 | ReqQuery extends GetReqQuery[T], 9 | ResBody extends GetResBody[T] 10 | >( 11 | key: T, 12 | { 13 | query, 14 | requestInit, 15 | swrConfig, 16 | }: { 17 | query?: ReqQuery; 18 | requestInit?: RequestInit; 19 | swrConfig?: SWRConfiguration; 20 | } = {} 21 | ) { 22 | const url = query ? `${key}?${qs.stringify(query)}` : key; 23 | return useSWR( 24 | url, 25 | async (): Promise => { 26 | const { data, error } = await fetch(url, requestInit).then((res) => 27 | res.json() 28 | ); 29 | if (error) throw error; 30 | return data; 31 | }, 32 | swrConfig 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/models/articles.ts: -------------------------------------------------------------------------------- 1 | export type Article = { 2 | id: string; 3 | title: string; 4 | body: string; 5 | }; 6 | 7 | export const articles: Article[] = [ 8 | { 9 | id: "123", 10 | title: "投稿1", 11 | body: "texttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttext", 12 | }, 13 | { 14 | id: "456", 15 | title: "投稿2", 16 | body: "texttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttext", 17 | }, 18 | { 19 | id: "789", 20 | title: "投稿3", 21 | body: "texttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttext", 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /src/models/users.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string; 3 | name: string; 4 | }; 5 | 6 | export const users: User[] = [ 7 | { id: "123", name: "taro" }, 8 | { id: "456", name: "jiro" }, 9 | { id: "789", name: "hanako" }, 10 | ]; 11 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import type { AppProps } from "next/app"; 3 | 4 | function MyApp({ Component, pageProps }: AppProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | 12 | export default MyApp; 13 | -------------------------------------------------------------------------------- /src/pages/api/articles/[id].ts: -------------------------------------------------------------------------------- 1 | import { Article, articles } from "@/models/articles"; 2 | import type { ApiHandler } from "@/types/pages/api"; 3 | // _____________________________________________________________________________ 4 | // 5 | export type GetHandler = ApiHandler<{ id: string }, {}, { article: Article }>; 6 | const getHandler: GetHandler = (req, res) => { 7 | if (!req.query.id) { 8 | res 9 | .status(400) 10 | .json({ error: { httpStatus: 400, message: "Invalid Request" } }); 11 | return; 12 | } 13 | const article = articles.find((article) => article.id === req.query.id); 14 | if (!article) { 15 | res.status(404).json({ error: { httpStatus: 404, message: "Not Found" } }); 16 | return; 17 | } 18 | res.status(200).json({ 19 | data: { article }, 20 | }); 21 | }; 22 | // _____________________________________________________________________________ 23 | // 24 | export type PutHandler = ApiHandler< 25 | { id: string }, 26 | { title: string; body: string }, 27 | { id: string; title: string; body: string } 28 | >; 29 | const putHandler: PutHandler = (req, res) => { 30 | if (!req.query.id || !req.body?.title || !req.body?.body) { 31 | res 32 | .status(400) 33 | .json({ error: { httpStatus: 400, message: "Invalid Request" } }); 34 | return; 35 | } 36 | res.status(200).json({ 37 | data: { 38 | id: req.query.id, 39 | title: req.body.title, 40 | body: req.body.body, 41 | }, 42 | }); 43 | }; 44 | // _____________________________________________________________________________ 45 | // 46 | export type DeleteHandler = ApiHandler<{ id: string }, {}, { id: string }>; 47 | const deleteHandler: DeleteHandler = (req, res) => { 48 | if (!req.query.id) { 49 | res 50 | .status(400) 51 | .json({ error: { httpStatus: 400, message: "Invalid Request" } }); 52 | return; 53 | } 54 | res.status(200).json({ data: { id: req.query.id } }); 55 | }; 56 | // _____________________________________________________________________________ 57 | // 58 | const handler: ApiHandler = (req, res) => { 59 | switch (req.method) { 60 | case "GET": 61 | getHandler(req, res); 62 | break; 63 | case "PUT": 64 | putHandler(req, res); 65 | break; 66 | case "DELETE": 67 | deleteHandler(req, res); 68 | break; 69 | default: 70 | res 71 | .status(405) 72 | .json({ error: { httpStatus: 405, message: "Method Not Allowed" } }); 73 | } 74 | }; 75 | export default handler; 76 | -------------------------------------------------------------------------------- /src/pages/api/articles/index.ts: -------------------------------------------------------------------------------- 1 | import { Article, articles } from "@/models/articles"; 2 | import type { ApiHandler } from "@/types/pages/api"; 3 | // _____________________________________________________________________________ 4 | // 5 | export type GetHandler = ApiHandler< 6 | { page?: string }, 7 | {}, 8 | { articles: Article[] } 9 | >; 10 | const getHandler: GetHandler = (_req, res) => { 11 | res.status(200).json({ data: { articles } }); 12 | }; 13 | // _____________________________________________________________________________ 14 | // 15 | export type PostHandler = ApiHandler< 16 | {}, 17 | { title: string; body: string }, 18 | { id: string; title: string; body: string } 19 | >; 20 | const postHandler: PostHandler = (req, res) => { 21 | if (!req.body?.title || !req.body?.body) { 22 | res.status(400).json({ 23 | error: { httpStatus: 400, message: "Invalid Request" }, 24 | }); 25 | return; 26 | } 27 | res 28 | .status(201) 29 | .json({ data: { id: "1", title: req.body.title, body: req.body.body } }); 30 | }; 31 | // _____________________________________________________________________________ 32 | // 33 | const handler: ApiHandler = (req, res) => { 34 | switch (req.method) { 35 | case "GET": 36 | getHandler(req, res); 37 | break; 38 | case "POST": 39 | postHandler(req, res); 40 | break; 41 | default: 42 | res 43 | .status(405) 44 | .json({ error: { httpStatus: 405, message: "Method Not Allowed" } }); 45 | } 46 | }; 47 | export default handler; 48 | -------------------------------------------------------------------------------- /src/pages/api/greet.ts: -------------------------------------------------------------------------------- 1 | import type { ApiHandler } from "@/types/pages/api"; 2 | // _____________________________________________________________________________ 3 | // 4 | export type GetHandler = ApiHandler<{ name: string }, {}, { message: string }>; 5 | const getHandler: GetHandler = (req, res) => { 6 | if (!req.query.name) { 7 | res 8 | .status(400) 9 | .json({ error: { httpStatus: 400, message: "Invalid Request" } }); 10 | return; 11 | } 12 | res.status(200).json({ data: { message: `hello ${req.query.name}` } }); 13 | }; 14 | // _____________________________________________________________________________ 15 | // 16 | const handler: ApiHandler = (req, res) => { 17 | switch (req.method) { 18 | case "GET": 19 | getHandler(req, res); 20 | break; 21 | default: 22 | res 23 | .status(405) 24 | .json({ error: { httpStatus: 405, message: "Method Not Allowed" } }); 25 | } 26 | }; 27 | export default handler; 28 | -------------------------------------------------------------------------------- /src/pages/api/users/[id].ts: -------------------------------------------------------------------------------- 1 | import { User, users } from "@/models/users"; 2 | import type { ApiHandler } from "@/types/pages/api"; 3 | // _____________________________________________________________________________ 4 | // 5 | export type GetHandler = ApiHandler<{ id: string }, {}, { user: User }>; 6 | const getHandler: GetHandler = (req, res) => { 7 | if (!req.query.id) { 8 | res 9 | .status(400) 10 | .json({ error: { httpStatus: 400, message: "Invalid Request" } }); 11 | return; 12 | } 13 | const user = users.find((user) => user.id === req.query.id); 14 | if (!user) { 15 | res.status(404).json({ error: { httpStatus: 404, message: "Not Found" } }); 16 | return; 17 | } 18 | res.status(200).json({ 19 | data: { user }, 20 | }); 21 | }; 22 | // _____________________________________________________________________________ 23 | // 24 | export type PutHandler = ApiHandler< 25 | { id: string }, 26 | { name: string }, 27 | { id: string; name: string } 28 | >; 29 | const putHandler: PutHandler = (req, res) => { 30 | if (!req.query.id || !req.body?.name) { 31 | res 32 | .status(400) 33 | .json({ error: { httpStatus: 400, message: "Invalid Request" } }); 34 | return; 35 | } 36 | res.status(200).json({ data: { id: req.query.id, name: req.body.name } }); 37 | }; 38 | // _____________________________________________________________________________ 39 | // 40 | export type DeleteHandler = ApiHandler<{ id: string }, {}, { id: string }>; 41 | const deleteHandler: DeleteHandler = (req, res) => { 42 | if (!req.query.id) { 43 | res 44 | .status(400) 45 | .json({ error: { httpStatus: 400, message: "Invalid Request" } }); 46 | return; 47 | } 48 | res.status(200).json({ data: { id: req.query.id } }); 49 | }; 50 | // _____________________________________________________________________________ 51 | // 52 | const handler: ApiHandler = (req, res) => { 53 | switch (req.method) { 54 | case "GET": 55 | getHandler(req, res); 56 | break; 57 | case "PUT": 58 | putHandler(req, res); 59 | break; 60 | case "DELETE": 61 | deleteHandler(req, res); 62 | break; 63 | default: 64 | res 65 | .status(405) 66 | .json({ error: { httpStatus: 405, message: "Method Not Allowed" } }); 67 | } 68 | }; 69 | export default handler; 70 | -------------------------------------------------------------------------------- /src/pages/api/users/index.ts: -------------------------------------------------------------------------------- 1 | import { User, users } from "@/models/users"; 2 | import type { ApiHandler } from "@/types/pages/api"; 3 | // _____________________________________________________________________________ 4 | // 5 | export type GetHandler = ApiHandler<{ page?: string }, {}, { users: User[] }>; 6 | const getHandler: GetHandler = (_req, res) => { 7 | res.status(200).json({ data: { users } }); 8 | }; 9 | // _____________________________________________________________________________ 10 | // 11 | export type PostHandler = ApiHandler< 12 | {}, 13 | { name: string }, 14 | { id: string; name: string } 15 | >; 16 | const postHandler: PostHandler = (req, res) => { 17 | if (!req.body?.name) { 18 | res.status(400).json({ 19 | error: { httpStatus: 400, message: "Invalid Request" }, 20 | }); 21 | return; 22 | } 23 | res.status(201).json({ data: { id: "1", name: req.body.name } }); 24 | }; 25 | // _____________________________________________________________________________ 26 | // 27 | const handler: ApiHandler = (req, res) => { 28 | switch (req.method) { 29 | case "GET": 30 | getHandler(req, res); 31 | break; 32 | case "POST": 33 | postHandler(req, res); 34 | break; 35 | default: 36 | res 37 | .status(405) 38 | .json({ error: { httpStatus: 405, message: "Method Not Allowed" } }); 39 | } 40 | }; 41 | export default handler; 42 | -------------------------------------------------------------------------------- /src/pages/articles/[id].tsx: -------------------------------------------------------------------------------- 1 | import { useApiData } from "@/hooks/useApiData"; 2 | import { deleteApiData } from "@/utils/fetcher"; 3 | import { useRouter } from "next/dist/client/router"; 4 | import React from "react"; 5 | // _____________________________________________________________________________ 6 | // 7 | const DeleteArticle = ({ id }: { id: string }) => { 8 | return ( 9 | 22 | ); 23 | }; 24 | 25 | const ArticleBase = ({ id }: { id: string }) => { 26 | const { data } = useApiData("/api/articles/[id]", { query: { id } }); 27 | if (!data) return <>...loading; 28 | return ( 29 |
30 |

{data.article.title}

31 |

{data.article.body}

32 |
33 | 34 |
35 | ); 36 | }; 37 | 38 | export const Article = () => { 39 | const { isReady, query } = useRouter(); 40 | if (!(isReady && typeof query.id === "string")) { 41 | return <>...loading; 42 | } 43 | return ; 44 | }; 45 | 46 | export default Article; 47 | -------------------------------------------------------------------------------- /src/pages/articles/index.tsx: -------------------------------------------------------------------------------- 1 | import { useApiData } from "@/hooks/useApiData"; 2 | import { postApiData } from "@/utils/fetcher"; 3 | import Link from "next/link"; 4 | import React from "react"; 5 | // _____________________________________________________________________________ 6 | // 7 | const CreateArticle = () => { 8 | const [title, setTitle] = React.useState(""); 9 | const [body, setBody] = React.useState(""); 10 | return ( 11 | <> 12 |
13 | { 18 | setTitle(e.target.value); 19 | }} 20 | /> 21 |
22 |
23 | 30 |
31 | 50 | 51 | ); 52 | }; 53 | 54 | export const Articles = () => { 55 | const { data } = useApiData("/api/articles"); 56 | if (!data) return <>...loading; 57 | return ( 58 |
59 |

Articles

60 |
    61 | {data.articles.map((article) => ( 62 |
  • 63 | 64 | {article.title} 65 | 66 |
  • 67 | ))} 68 |
69 |
70 | 71 |
72 | ); 73 | }; 74 | 75 | export default Articles; 76 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useApiData } from "@/hooks/useApiData"; 2 | import React from "react"; 3 | // _____________________________________________________________________________ 4 | // 5 | const Greet = () => { 6 | const { data } = useApiData("/api/greet", { query: { name: "user" } }); 7 | if (!data) return <>...loading; 8 | return ( 9 |
10 |

Greet

11 |

{data.message}

12 |
13 | ); 14 | }; 15 | 16 | export default Greet; 17 | -------------------------------------------------------------------------------- /src/pages/users/[id].tsx: -------------------------------------------------------------------------------- 1 | import { useApiData } from "@/hooks/useApiData"; 2 | import { deleteApiData } from "@/utils/fetcher"; 3 | import { useRouter } from "next/dist/client/router"; 4 | import React from "react"; 5 | // _____________________________________________________________________________ 6 | // 7 | const DeleteUser = ({ id }: { id: string }) => { 8 | return ( 9 | 22 | ); 23 | }; 24 | 25 | const UserBase = ({ id }: { id: string }) => { 26 | const { data, error } = useApiData("/api/users/[id]", { 27 | query: { id }, 28 | }); 29 | if (error) { 30 | return

{error.httpStatus}

; 31 | } 32 | if (!data) return <>...loading; 33 | return ( 34 |
35 |

User

36 |

{data.user.name}

37 |
38 | 39 |
40 | ); 41 | }; 42 | 43 | export const User = () => { 44 | const { isReady, query } = useRouter(); 45 | if (!(isReady && typeof query.id === "string")) { 46 | return <>...loading; 47 | } 48 | return ; 49 | }; 50 | 51 | export default User; 52 | -------------------------------------------------------------------------------- /src/pages/users/index.tsx: -------------------------------------------------------------------------------- 1 | import { useApiData } from "@/hooks/useApiData"; 2 | import { postApiData } from "@/utils/fetcher"; 3 | import Link from "next/link"; 4 | import React from "react"; 5 | // _____________________________________________________________________________ 6 | // 7 | const CreateUser = () => { 8 | const [name, setName] = React.useState(""); 9 | return ( 10 | <> 11 | { 16 | setName(e.target.value); 17 | }} 18 | /> 19 | 35 | 36 | ); 37 | }; 38 | 39 | export const Users = () => { 40 | const { data } = useApiData("/api/users"); 41 | if (!data) return <>...loading; 42 | return ( 43 |
44 |

Users

45 |
    46 | {data.users.map((user) => ( 47 |
  • 48 | 49 | {user.name} 50 | 51 |
  • 52 | ))} 53 |
54 |
55 | 56 |
57 | ); 58 | }; 59 | 60 | export default Users; 61 | -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takefumi-yoshii/nextjs-typesafe-api-route/31bfd439c57f193749e8860e467292e2428bc2a4/src/public/favicon.ico -------------------------------------------------------------------------------- /src/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/types/api.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@/types/pages/api" { 2 | export interface Data { 3 | data: T; 4 | } 5 | export interface Error { 6 | error: { 7 | httpStatus: number; 8 | message: string; 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/fetcher.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DeleteReqBody, 3 | DeleteReqQuery, 4 | DeleteResBody, 5 | PatchReqBody, 6 | PatchReqQuery, 7 | PatchResBody, 8 | PostReqBody, 9 | PostReqQuery, 10 | PostResBody, 11 | PutReqBody, 12 | PutReqQuery, 13 | PutResBody, 14 | } from "@/types/pages/api"; 15 | import qs from "query-string"; 16 | // _____________________________________________________________________________ 17 | // 18 | const defaultHeaders = { 19 | "Content-Type": "application/json", 20 | }; 21 | // _____________________________________________________________________________ 22 | // 23 | export async function postApiData< 24 | T extends keyof PostResBody, 25 | ReqQuery extends PostReqQuery[T], 26 | ResBody extends PostResBody[T], 27 | ReqBody extends PostReqBody[T] 28 | >( 29 | key: T, 30 | { 31 | query, 32 | requestInit, 33 | }: { 34 | query?: ReqQuery; 35 | requestInit?: Omit & { body?: ReqBody }; 36 | } = {} 37 | ): Promise { 38 | const url = query ? `${key}?${qs.stringify(query)}` : key; 39 | const { data, error } = await fetch(url, { 40 | ...requestInit, 41 | method: "POST", 42 | headers: { ...defaultHeaders, ...requestInit?.headers }, 43 | body: requestInit?.body ? JSON.stringify(requestInit.body) : undefined, 44 | }).then((res) => res.json()); 45 | if (error) throw error; 46 | return data; 47 | } 48 | // _____________________________________________________________________________ 49 | // 50 | export async function putApiData< 51 | T extends keyof PutResBody, 52 | ReqQuery extends PutReqQuery[T], 53 | ResBody extends PutResBody[T], 54 | ReqBody extends PutReqBody[T] 55 | >( 56 | key: T, 57 | { 58 | query, 59 | requestInit, 60 | }: { 61 | query?: ReqQuery; 62 | requestInit?: Omit & { body?: ReqBody }; 63 | } = {} 64 | ): Promise { 65 | const url = query ? `${key}?${qs.stringify(query)}` : key; 66 | const { data, error } = await fetch(url, { 67 | ...requestInit, 68 | method: "PUT", 69 | headers: { ...defaultHeaders, ...requestInit?.headers }, 70 | body: requestInit?.body ? JSON.stringify(requestInit.body) : undefined, 71 | }).then((res) => res.json()); 72 | if (error) throw error; 73 | return data; 74 | } 75 | // _____________________________________________________________________________ 76 | // 77 | export async function patchApiData< 78 | T extends keyof PatchResBody, 79 | ReqQuery extends PatchReqQuery[T], 80 | ResBody extends PatchResBody[T], 81 | ReqBody extends PatchReqBody[T] 82 | >( 83 | key: T, 84 | { 85 | query, 86 | requestInit, 87 | }: { 88 | query?: ReqQuery; 89 | requestInit?: Omit & { body?: ReqBody }; 90 | } = {} 91 | ): Promise { 92 | const url = query ? `${key}?${qs.stringify(query)}` : key; 93 | const { data, error } = await fetch(url, { 94 | ...requestInit, 95 | method: "PATCH", 96 | headers: { ...defaultHeaders, ...requestInit?.headers }, 97 | body: requestInit?.body ? JSON.stringify(requestInit.body) : undefined, 98 | }).then((res) => res.json()); 99 | if (error) throw error; 100 | return data; 101 | } 102 | // _____________________________________________________________________________ 103 | // 104 | export async function deleteApiData< 105 | T extends keyof DeleteResBody, 106 | ReqQuery extends DeleteReqQuery[T], 107 | ResBody extends DeleteResBody[T], 108 | ReqBody extends DeleteReqBody[T] 109 | >( 110 | key: T, 111 | { 112 | query, 113 | requestInit, 114 | }: { 115 | query?: ReqQuery; 116 | requestInit?: Omit & { body?: ReqBody }; 117 | } = {} 118 | ): Promise { 119 | const url = query ? `${key}?${qs.stringify(query)}` : key; 120 | const { data, error } = await fetch(url, { 121 | ...requestInit, 122 | method: "DELETE", 123 | headers: { ...defaultHeaders, ...requestInit?.headers }, 124 | body: requestInit?.body ? JSON.stringify(requestInit.body) : undefined, 125 | }).then((res) => res.json()); 126 | if (error) throw error; 127 | return data; 128 | } 129 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "incremental": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "baseUrl": ".", 22 | "paths": { 23 | "@/*": [ 24 | "./src/*" 25 | ], 26 | "~/*": [ 27 | "./lib/*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | --------------------------------------------------------------------------------