├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── dev.to ├── app-auth │ ├── .gitignore │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── client │ │ │ ├── app.jsx │ │ │ ├── main.tsx │ │ │ └── user.jsx │ │ ├── server │ │ │ ├── api-admin │ │ │ │ ├── profile │ │ │ │ │ └── CRUD.js │ │ │ │ └── user │ │ │ │ │ └── CRUD.js │ │ │ ├── api-dev │ │ │ │ ├── GET.js │ │ │ │ └── sync-model │ │ │ │ │ └── GET.js │ │ │ ├── db │ │ │ │ ├── database.ts │ │ │ │ ├── index.ts │ │ │ │ └── model │ │ │ │ │ ├── Profile.ts │ │ │ │ │ └── User.ts │ │ │ ├── restful │ │ │ │ ├── common.ts │ │ │ │ ├── createOne.ts │ │ │ │ ├── deleteOne.ts │ │ │ │ ├── getMany.ts │ │ │ │ ├── getOne.ts │ │ │ │ ├── index.ts │ │ │ │ └── updateOne.ts │ │ │ └── types.ts │ │ └── vite-env.d.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── yarn.lock └── app-crud │ ├── .gitignore │ ├── README.md │ ├── data │ └── users.db │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── client │ │ ├── api │ │ │ ├── dataProvider.js │ │ │ └── index.js │ │ ├── main.jsx │ │ └── user-admin │ │ │ ├── form.jsx │ │ │ ├── index.jsx │ │ │ └── table.jsx │ └── server │ │ ├── api-dev │ │ └── GET.js │ │ ├── api-prod │ │ └── GET.js │ │ ├── api │ │ ├── AUTH.js │ │ ├── ERROR.js │ │ └── user │ │ │ ├── GET.js │ │ │ ├── POST.js │ │ │ └── [userId] │ │ │ ├── DELETE.js │ │ │ ├── GET.js │ │ │ └── PUT.js │ │ ├── configure.js │ │ └── db │ │ └── index.js │ ├── vite.config.js │ └── yarn.lock ├── package.json ├── packages ├── examples │ ├── app │ │ ├── .gitignore │ │ ├── README.md │ │ ├── data │ │ │ ├── products.db │ │ │ └── users.db │ │ ├── eslint.config.js │ │ ├── index.html │ │ ├── package.json │ │ ├── public │ │ │ └── vite.svg │ │ ├── src │ │ │ ├── client │ │ │ │ ├── api │ │ │ │ │ ├── dataProvider.js │ │ │ │ │ └── index.js │ │ │ │ ├── hooks │ │ │ │ │ └── index.js │ │ │ │ ├── main.jsx │ │ │ │ └── user-admin │ │ │ │ │ └── index.jsx │ │ │ └── server │ │ │ │ ├── api │ │ │ │ └── user │ │ │ │ │ ├── [id] │ │ │ │ │ └── index.js │ │ │ │ │ └── index.js │ │ │ │ ├── configure.js │ │ │ │ └── db │ │ │ │ └── index.js │ │ ├── vite.config.js │ │ └── yarn.lock │ ├── appIsolated │ │ ├── .gitignore │ │ ├── README.md │ │ ├── data │ │ │ ├── products.db │ │ │ └── users.db │ │ ├── eslint.config.js │ │ ├── index.html │ │ ├── package.json │ │ ├── public │ │ │ └── vite.svg │ │ ├── src │ │ │ ├── client │ │ │ │ ├── api │ │ │ │ │ ├── dataProvider.js │ │ │ │ │ └── index.js │ │ │ │ ├── hooks │ │ │ │ │ └── index.js │ │ │ │ ├── main.jsx │ │ │ │ └── user-admin │ │ │ │ │ └── index.jsx │ │ │ └── server │ │ │ │ ├── api │ │ │ │ └── user │ │ │ │ │ ├── GET.js │ │ │ │ │ ├── POST.js │ │ │ │ │ ├── USE.js │ │ │ │ │ └── [userId] │ │ │ │ │ ├── DELETE.js │ │ │ │ │ ├── GET.js │ │ │ │ │ ├── PUT.js │ │ │ │ │ └── USE.js │ │ │ │ ├── configure.js │ │ │ │ └── db │ │ │ │ └── index.js │ │ └── vite.config.js │ ├── basic │ │ ├── .env │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── private │ │ │ ├── .env │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── public │ │ │ └── favicon.ico │ │ ├── src │ │ │ ├── api │ │ │ │ ├── admin.ts │ │ │ │ ├── admin │ │ │ │ │ ├── post │ │ │ │ │ │ ├── [postId] │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── user │ │ │ │ │ │ ├── [userId] │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── index.ts │ │ │ │ ├── auth │ │ │ │ │ ├── login.ts │ │ │ │ │ ├── logout.ts │ │ │ │ │ └── supplant.ts │ │ │ │ ├── routers.ts │ │ │ │ ├── site.ts │ │ │ │ └── site │ │ │ │ │ ├── article │ │ │ │ │ ├── $articleId.js │ │ │ │ │ ├── new.js │ │ │ │ │ └── zip.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── page │ │ │ │ │ └── $pageId.js │ │ │ ├── common │ │ │ │ ├── authMiddleware.ts │ │ │ │ └── response.ts │ │ │ ├── custom-server-example │ │ │ │ ├── configure.ts │ │ │ │ ├── handler.ts │ │ │ │ └── server.ts │ │ │ ├── main.tsx │ │ │ ├── middleware │ │ │ │ ├── jwt.ts │ │ │ │ └── sessionToken.ts │ │ │ ├── vite-env.d.ts │ │ │ └── web │ │ │ │ └── App.tsx │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ ├── vite.config.ts │ │ └── yarn.lock │ └── dual │ │ ├── .gitignore │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── index.html │ │ ├── package.json │ │ ├── public │ │ └── vite.svg │ │ ├── src │ │ ├── api-isolated │ │ │ ├── AUTH.js │ │ │ ├── ERROR.js │ │ │ ├── GET.js │ │ │ ├── USE.js │ │ │ └── user │ │ │ │ ├── ERROR.js │ │ │ │ ├── GET.js │ │ │ │ ├── LOGGER.js │ │ │ │ ├── POST.js │ │ │ │ ├── [id] │ │ │ │ ├── DELETE.js │ │ │ │ ├── PATCH.js │ │ │ │ ├── PUT.js │ │ │ │ └── USE.js │ │ │ │ └── confirm │ │ │ │ └── POST.js │ │ ├── api-legacy │ │ │ ├── index.js │ │ │ └── user │ │ │ │ ├── [id] │ │ │ │ └── index.js │ │ │ │ ├── confirm.js │ │ │ │ └── index.js │ │ ├── main.tsx │ │ └── vite-env.d.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ ├── vite.config.ts │ │ └── yarn.lock └── plugin │ ├── .gitignore │ ├── README.md │ ├── README_1.0.md │ ├── package.json │ ├── src │ ├── api-server │ │ ├── configure.ts │ │ ├── env.ts │ │ ├── handler.ts │ │ ├── routers.ts │ │ └── server.ts │ ├── env.d.ts │ ├── index.ts │ ├── model.ts │ ├── plugin-build │ │ └── index.ts │ ├── plugin-route │ │ ├── common.ts │ │ ├── index.ts │ │ └── routersFile.ts │ ├── plugin-serve │ │ └── index.ts │ └── utils.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── tutorial-crud.md │ ├── tutorial-isolated.md │ ├── tutorial-legacy.md │ └── yarn.lock └── yarn.lock /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: "npm publish" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: 20 19 | registry-url: https://registry.npmjs.org 20 | 21 | - name: Install dependencies and publish package 22 | run: | 23 | cd packages/plugin 24 | npm install --force 25 | npm publish 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.api 2 | **/*.tgz 3 | **/.yarn 4 | **/.up-force 5 | **/.fetch-client 6 | **/node_modules -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "default": "Run Build", 4 | "configurations": [ 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "Run App", 9 | "skipFiles": [ 10 | "/**" 11 | ], 12 | "runtimeExecutable": "npx", 13 | "args": [ 14 | "vite", 15 | "dev" 16 | ], 17 | "env": { 18 | "NODE_ENV": "development" 19 | }, 20 | "cwd": "${workspaceFolder}/packages/examples/app", 21 | "autoAttachChildProcesses": true 22 | }, 23 | { 24 | "type": "node", 25 | "request": "launch", 26 | "name": "Run Build", 27 | "skipFiles": [ 28 | "/**" 29 | ], 30 | "runtimeExecutable": "npx", 31 | "args": [ 32 | "vite", 33 | "build" 34 | ], 35 | "env": { 36 | "NODE_ENV": "production" 37 | }, 38 | "cwd": "${workspaceFolder}/packages/examples/app", 39 | "console": "integratedTerminal", 40 | "autoAttachChildProcesses": true 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.exclude": { 4 | "**/tsconfig.json": false, 5 | "**/tsconfig.node.json": true, 6 | "**/node_modules": false 7 | }, 8 | "editor.codeActionsOnSave": { 9 | "source.organizeImports": "explicit" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Willyams Yujra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Plugins 2 | 3 | This repository contains multiple projects, each providing specific functionality for enhancing development workflows. Below, you'll find the available plugins along with links to their documentation and resources. 4 | 5 | ## Available Plugins 6 | 7 | ### [vite-plugin-api-routes](packages/vite-plugin-api-routes/README.md) 8 | 9 | A Vite plugin that enhances API routing by leveraging directory structure to automatically generate API routes. 10 | 11 | - **Description:** `vite-plugin-api-routes` simplifies API route creation by converting the directory structure into route rules. It's perfect for projects that require dynamic API routes, inspired by frameworks like Next.js and Remix. 12 | - **Installation:** 13 | 14 | ``` 15 | yarn add vite-plugin-api-routes 16 | ``` 17 | 18 | - **Documentation and Resources:** 19 | - [NPM Package](https://www.npmjs.com/package/vite-plugin-api-routes) 20 | - [GitHub Repository](https://github.com/yracnet/vite-plugin-api-routes) 21 | - [Dev.to Article: Enhancing API Routing in Vite.js](https://dev.to/yracnet/enhancing-api-routing-in-vitejs-with-vite-plugin-api-p39) 22 | - [Dev.to Article: CRUD User API + GUI in ViteJS](https://dev.to/yracnet/crud-user-api-gui-in-vitejs-df8) 23 | - [Tutorial Legacy](./packages/plugin/tutorial-legacy.md) 24 | - [Tutorial Isolated](./packages/plugin/tutorial-isolated.md) 25 | - [Tutorial CRUD](./packages/plugin/tutorial-crud.md) 26 | 27 | --- 28 | -------------------------------------------------------------------------------- /dev.to/app-auth/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .api 26 | db -------------------------------------------------------------------------------- /dev.to/app-auth/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 13 | 14 | ```js 15 | export default tseslint.config({ 16 | extends: [ 17 | // Remove ...tseslint.configs.recommended and replace with this 18 | ...tseslint.configs.recommendedTypeChecked, 19 | // Alternatively, use this for stricter rules 20 | ...tseslint.configs.strictTypeChecked, 21 | // Optionally, add this for stylistic rules 22 | ...tseslint.configs.stylisticTypeChecked, 23 | ], 24 | languageOptions: { 25 | // other options... 26 | parserOptions: { 27 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 28 | tsconfigRootDir: import.meta.dirname, 29 | }, 30 | }, 31 | }) 32 | ``` 33 | 34 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 35 | 36 | ```js 37 | // eslint.config.js 38 | import reactX from 'eslint-plugin-react-x' 39 | import reactDom from 'eslint-plugin-react-dom' 40 | 41 | export default tseslint.config({ 42 | plugins: { 43 | // Add the react-x and react-dom plugins 44 | 'react-x': reactX, 45 | 'react-dom': reactDom, 46 | }, 47 | rules: { 48 | // other rules... 49 | // Enable its recommended typescript rules 50 | ...reactX.configs['recommended-typescript'].rules, 51 | ...reactDom.configs.recommended.rules, 52 | }, 53 | }) 54 | ``` 55 | -------------------------------------------------------------------------------- /dev.to/app-auth/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /dev.to/app-auth/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /dev.to/app-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-auth", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^19.0.0", 14 | "react-dom": "^19.0.0" 15 | }, 16 | "devDependencies": { 17 | "@eslint/js": "^9.21.0", 18 | "@types/react": "^19.0.10", 19 | "@types/react-dom": "^19.0.4", 20 | "@vitejs/plugin-react-swc": "^3.8.0", 21 | "dotenv-local": "^1.0.2", 22 | "eslint": "^9.21.0", 23 | "eslint-plugin-react-hooks": "^5.1.0", 24 | "eslint-plugin-react-refresh": "^0.4.19", 25 | "express": "^4.21.2", 26 | "globals": "^15.15.0", 27 | "jwt-decode": "^4.0.0", 28 | "ra-core": "^5.6.4", 29 | "ra-data-simple-rest": "^5.6.4", 30 | "react-hook-form": "^7.54.2", 31 | "react-router": "^7.4.0", 32 | "react-router-dom": "^7.4.0", 33 | "sequelize": "^6.37.6", 34 | "sqlite": "^5.1.1", 35 | "sqlite3": "^5.1.7", 36 | "typescript": "~5.7.2", 37 | "typescript-eslint": "^8.24.1", 38 | "vite": "^6.2.2", 39 | "vite-plugin-api-routes": "^1.2.3-beta", 40 | "vite-tsconfig-paths": "^5.1.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dev.to/app-auth/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/client/app.jsx: -------------------------------------------------------------------------------- 1 | import { CoreAdminContext, Resource } from "ra-core"; 2 | import apiServerProvider from "ra-data-simple-rest"; 3 | import { UserCreate, UserEdit, UserList, UserShow } from "./user"; 4 | 5 | const dataProvider = apiServerProvider("/api/admin"); 6 | 7 | const App = () => { 8 | return ( 9 | 10 | 17 | 18 | ); 19 | }; 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/client/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./app"; 4 | 5 | createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/client/user.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | ListBase, 3 | RecordContextProvider, 4 | useFieldValue, 5 | useListContext, 6 | useRecordContext, 7 | } from "ra-core"; 8 | 9 | const Column = (props) => { 10 | const value = useFieldValue(props); 11 | return
xxx{JSON.stringify(value)}
; 12 | }; 13 | 14 | const Row = () => { 15 | const record = useRecordContext(); 16 | return
{JSON.stringify(record)}
; 17 | }; 18 | 19 | const Grid = (props) => { 20 | const { data, isPending } = useListContext(); 21 | return ( 22 | <> 23 | {isPending ? ( 24 | Cargando... 25 | ) : ( 26 | data.map((it) => ( 27 | 28 | {props.children} 29 | 30 | )) 31 | )} 32 | 33 | ); 34 | }; 35 | 36 | export const UserList = () => { 37 | return ( 38 | 39 | UserList 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | export const UserCreate = () => { 49 | return UserCreate; 50 | }; 51 | export const UserEdit = () => { 52 | return UserEdit; 53 | }; 54 | export const UserShow = () => { 55 | return UserShow; 56 | }; 57 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/api-admin/profile/CRUD.js: -------------------------------------------------------------------------------- 1 | import { profileRepository } from "_server/db"; 2 | import { createCRUDRouter } from "_server/restful"; 3 | 4 | export default createCRUDRouter(profileRepository); 5 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/api-admin/user/CRUD.js: -------------------------------------------------------------------------------- 1 | import { userRepository } from "_server/db"; 2 | import { createCRUDRouter } from "_server/restful"; 3 | 4 | export default createCRUDRouter(userRepository); 5 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/api-dev/GET.js: -------------------------------------------------------------------------------- 1 | export default (_, res) => { 2 | res.send("PING DEV"); 3 | }; 4 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/api-dev/sync-model/GET.js: -------------------------------------------------------------------------------- 1 | import { instance, profileRepository, userRepository } from "_server/db"; 2 | 3 | export default async (_, res) => { 4 | await userRepository.sync({ force: true }); 5 | await profileRepository.sync({ force: true }); 6 | 7 | const tablas = await instance.getQueryInterface().showAllSchemas(); 8 | res.send({ name: "SYNC MODEL DEV", tablas }); 9 | }; 10 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/db/database.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { DataTypes, Sequelize } from "sequelize"; 3 | export const instance = new Sequelize({ 4 | dialect: "sqlite", 5 | storage: path.join(process.cwd(), "db/data.db"), 6 | logging: false, 7 | }); 8 | 9 | export const DEFINE_ID = { 10 | primaryKey: true, 11 | type: DataTypes.UUID, 12 | defaultValue: DataTypes.UUIDV4, 13 | allowNull: false, 14 | }; 15 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/db/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./database.ts"; 2 | export * from "./model/Profile.ts"; 3 | export * from "./model/User.ts"; 4 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/db/model/Profile.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreationOptional, 3 | DataTypes, 4 | InferAttributes, 5 | InferCreationAttributes, 6 | Model, 7 | } from "sequelize"; 8 | import { DEFINE_ID, instance } from "../database"; 9 | 10 | export interface ProfileModel 11 | extends Model< 12 | InferAttributes, 13 | InferCreationAttributes 14 | > { 15 | id: CreationOptional; 16 | name: string; 17 | description: string; 18 | status: string; 19 | } 20 | 21 | export const profileRepository = instance.define( 22 | "Profile", 23 | { 24 | id: DEFINE_ID, 25 | name: { 26 | type: DataTypes.STRING, 27 | allowNull: false, 28 | unique: true, 29 | }, 30 | description: { 31 | type: DataTypes.STRING, 32 | allowNull: false, 33 | }, 34 | status: { 35 | type: DataTypes.STRING, 36 | allowNull: false, 37 | }, 38 | }, 39 | { 40 | tableName: "auth_profiles", 41 | createdAt: true, 42 | updatedAt: true, 43 | } 44 | ); 45 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/db/model/User.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreationOptional, 3 | DataTypes, 4 | InferAttributes, 5 | InferCreationAttributes, 6 | Model, 7 | } from "sequelize"; 8 | import { DEFINE_ID, instance } from "../database"; 9 | 10 | export interface UserModel 11 | extends Model< 12 | InferAttributes, 13 | InferCreationAttributes 14 | > { 15 | id: CreationOptional; 16 | referenceId: number; 17 | email: string; 18 | username: string; 19 | password: string; 20 | profile: string; 21 | status: string; 22 | } 23 | 24 | export const userRepository = instance.define( 25 | "User", 26 | { 27 | id: DEFINE_ID, 28 | referenceId: { 29 | type: DataTypes.UUID, 30 | allowNull: false, 31 | }, 32 | email: { 33 | type: DataTypes.STRING, 34 | allowNull: false, 35 | unique: true, 36 | }, 37 | username: { 38 | type: DataTypes.STRING, 39 | allowNull: false, 40 | unique: true, 41 | }, 42 | password: { 43 | type: DataTypes.STRING, 44 | allowNull: false, 45 | }, 46 | profile: { 47 | type: DataTypes.STRING, 48 | allowNull: false, 49 | }, 50 | status: { 51 | type: DataTypes.STRING, 52 | allowNull: false, 53 | }, 54 | }, 55 | { 56 | tableName: "auth_users", 57 | createdAt: true, 58 | updatedAt: true, 59 | } 60 | ); 61 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/restful/common.ts: -------------------------------------------------------------------------------- 1 | import { Op } from "sequelize"; 2 | 3 | export const NOPE = (v: any) => v; 4 | 5 | export type QueryParams = any & { 6 | sort?: string; 7 | range?: string; 8 | filter?: string; 9 | q?: string; 10 | }; 11 | 12 | export type QueryOptions = { 13 | qcols?: string[]; 14 | }; 15 | 16 | export const parseQuery = ( 17 | { 18 | sort = '["id", "desc"]', 19 | range = "[0, 9]", 20 | filter = "{}", 21 | q = "", 22 | }: QueryParams, 23 | { qcols = [] }: QueryOptions = {} 24 | ) => { 25 | const _sort = JSON.parse(sort); 26 | const [offset = 0, limit = 9] = JSON.parse(range); 27 | const _filter = JSON.parse(filter) || {}; 28 | const where: any = { 29 | [Op.and]: [], 30 | }; 31 | where[Op.and] = Object.entries(_filter).map(([name, value]) => { 32 | if (Array.isArray(value)) { 33 | value = { 34 | [Op.in]: value, 35 | }; 36 | } 37 | return { 38 | [name]: value, 39 | }; 40 | }); 41 | if (q) { 42 | where[Op.and].push({ 43 | [Op.or]: qcols.map((name: string) => { 44 | return { 45 | [name]: { [Op.like]: `%${q}%` }, 46 | }; 47 | }), 48 | }); 49 | } 50 | return { 51 | offset, 52 | limit, 53 | where, 54 | order: [_sort], 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/restful/createOne.ts: -------------------------------------------------------------------------------- 1 | import { ApiFunction } from "_server/types"; 2 | import { Model, ModelStatic } from "sequelize"; 3 | import { NOPE } from "./common"; 4 | 5 | export type CreateOneOptions = { 6 | parsePayload?: (data: R) => Promise | R; 7 | }; 8 | 9 | export const createCreateOne = ( 10 | handler: ModelStatic, 11 | { parsePayload = NOPE }: CreateOneOptions 12 | ): ApiFunction => { 13 | return async (req, res, next) => { 14 | try { 15 | const data: any = await parsePayload(req.body); 16 | const newItem = await handler.create(data); 17 | res.status(201).json(newItem); 18 | } catch (error) { 19 | next(error); 20 | } 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/restful/deleteOne.ts: -------------------------------------------------------------------------------- 1 | import { ApiFunction } from "_server/types"; 2 | import { Model, ModelStatic } from "sequelize"; 3 | 4 | export type DeleteOneOptions = { 5 | paramPk?: string; 6 | }; 7 | 8 | export const createDeleteOne = ( 9 | handler: ModelStatic, 10 | { paramPk = "id" }: DeleteOneOptions 11 | ): ApiFunction => { 12 | return async (req, res, next) => { 13 | try { 14 | const pk = req.params[paramPk]; 15 | const item = await handler.findByPk(pk); 16 | if (item) { 17 | await item.destroy(); 18 | res.sendStatus(204); 19 | } else { 20 | res.sendStatus(404); 21 | } 22 | } catch (error) { 23 | next(error); 24 | } 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/restful/getMany.ts: -------------------------------------------------------------------------------- 1 | import { ApiFunction } from "_server/types"; 2 | import { Model, ModelStatic } from "sequelize"; 3 | import { parseQuery, QueryOptions } from "./common"; 4 | 5 | export type GetManyOptions = { 6 | queryOpts?: QueryOptions; 7 | }; 8 | 9 | export const createGetMany = ( 10 | handler: ModelStatic, 11 | { queryOpts }: GetManyOptions 12 | ): ApiFunction => { 13 | return async (req, res, next) => { 14 | try { 15 | const { 16 | offset = 0, 17 | limit = 10, 18 | where = {}, 19 | order = [], 20 | } = parseQuery(req.query, queryOpts); 21 | const { count, rows } = await handler.findAndCountAll({ 22 | where, 23 | order, 24 | offset, 25 | limit, 26 | }); 27 | res.setHeader("Content-Range", `rows ${offset}-${limit}/${count}`); 28 | res.json(rows); 29 | } catch (error) { 30 | next(error); 31 | } 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/restful/getOne.ts: -------------------------------------------------------------------------------- 1 | import { ApiFunction } from "_server/types"; 2 | import { Model, ModelStatic } from "sequelize"; 3 | 4 | export type GetOneOptions = { 5 | paramPk?: string; 6 | }; 7 | 8 | export const createGetOne = ( 9 | handler: ModelStatic, 10 | { paramPk = "id" }: GetOneOptions 11 | ): ApiFunction => { 12 | return async (req, res, next) => { 13 | try { 14 | const id = req.params[paramPk]; 15 | const rol = await handler.findByPk(id); 16 | if (rol) { 17 | res.json(rol); 18 | } else { 19 | res.sendStatus(404); 20 | } 21 | } catch (error) { 22 | next(error); 23 | } 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/restful/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { Model, ModelStatic } from "sequelize"; 3 | import { createCreateOne, CreateOneOptions } from "./createOne"; 4 | import { createDeleteOne, DeleteOneOptions } from "./deleteOne"; 5 | import { createGetMany, GetManyOptions } from "./getMany"; 6 | import { createGetOne, GetOneOptions } from "./getOne"; 7 | import { createUpdateOne, UpdateOneOptions } from "./updateOne"; 8 | 9 | export type CRUDRouterOptions = { 10 | createOneOpts?: false | CreateOneOptions; 11 | updateOneOpts?: false | UpdateOneOptions; 12 | deleteOneOpts?: false | DeleteOneOptions; 13 | getOneOpts?: false | GetOneOptions; 14 | getManyOpts?: false | GetManyOptions; 15 | }; 16 | 17 | export const createCRUDRouter = ( 18 | handler: ModelStatic, 19 | { 20 | createOneOpts = {}, 21 | updateOneOpts = {}, 22 | deleteOneOpts = {}, 23 | getManyOpts = {}, 24 | getOneOpts = {}, 25 | }: CRUDRouterOptions = {} 26 | ) => { 27 | const router = Router(); 28 | getManyOpts && router.get("/", createGetMany(handler, getManyOpts)); 29 | createOneOpts && router.post("/", createCreateOne(handler, createOneOpts)); 30 | getOneOpts && router.get("/:id", createGetOne(handler, getOneOpts)); 31 | updateOneOpts && router.put("/:id", createUpdateOne(handler, updateOneOpts)); 32 | deleteOneOpts && 33 | router.delete("/:id", createDeleteOne(handler, deleteOneOpts)); 34 | return router; 35 | }; 36 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/restful/updateOne.ts: -------------------------------------------------------------------------------- 1 | import { ApiFunction } from "_server/types"; 2 | import { Model, ModelStatic } from "sequelize"; 3 | import { NOPE } from "./common"; 4 | 5 | export type UpdateOneOptions = { 6 | paramPk?: string; 7 | parsePayload?: (data: R) => Promise | R; 8 | }; 9 | 10 | export const createUpdateOne = ( 11 | handler: ModelStatic, 12 | { paramPk = "id", parsePayload = NOPE }: UpdateOneOptions 13 | ): ApiFunction => { 14 | return async (req, res, next) => { 15 | try { 16 | const pk = req.params[paramPk]; 17 | const data = await parsePayload(req.body); 18 | const item = await handler.findByPk(pk); 19 | if (item) { 20 | await item.update(data); 21 | res.json(item); 22 | } else { 23 | res.sendStatus(404); 24 | } 25 | } catch (error) { 26 | next(error); 27 | } 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/server/types.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | export type ApiFunction< 4 | Param = any, 5 | Result = any, 6 | Payload = any, 7 | Query = any 8 | > = ( 9 | req: Request, 10 | res: Response, 11 | next: NextFunction 12 | ) => Promise | void | any | Promise; 13 | 14 | export type ApiErrorFunction< 15 | Param = any, 16 | Result = any, 17 | Payload = any, 18 | Query = any 19 | > = ( 20 | error: any, 21 | req: Request, 22 | res: Response, 23 | next: NextFunction 24 | ) => Promise | void; 25 | -------------------------------------------------------------------------------- /dev.to/app-auth/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /dev.to/app-auth/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": [ 7 | "ES2020", 8 | "DOM", 9 | "DOM.Iterable" 10 | ], 11 | "module": "ESNext", 12 | "skipLibCheck": true, 13 | /* Bundler mode */ 14 | "moduleResolution": "bundler", 15 | "allowImportingTsExtensions": true, 16 | "allowJs": true, 17 | "isolatedModules": true, 18 | "moduleDetection": "force", 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | /* Linting */ 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "noUncheckedSideEffectImports": true, 27 | /* Alias */ 28 | "paths": { 29 | "_client/*": [ 30 | "./src/client/*" 31 | ], 32 | "_server/*": [ 33 | "./src/server/*" 34 | ], 35 | } 36 | }, 37 | "include": [ 38 | "src" 39 | ] 40 | } -------------------------------------------------------------------------------- /dev.to/app-auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /dev.to/app-auth/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /dev.to/app-auth/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react-swc"; 2 | import { defineConfig } from "vite"; 3 | import api from "vite-plugin-api-routes"; 4 | import ts from "vite-tsconfig-paths"; 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | ts(), 10 | react(), 11 | api({ 12 | mode: "isolated", 13 | //configure: "src/server/configure.js", 14 | mapper: { 15 | AUTH: { method: "use", priority: 11 }, 16 | CRUD: { method: "use", priority: 12 }, 17 | ERROR: { method: "use", priority: 110 }, 18 | }, 19 | dirs: [ 20 | { dir: "src/server/api-admin", route: "admin" }, 21 | { dir: "src/server/api-auth", route: "auth" }, 22 | { dir: "src/server/api-dev", route: "_", skip: "production" }, 23 | ], 24 | }), 25 | ], 26 | }); 27 | -------------------------------------------------------------------------------- /dev.to/app-crud/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /dev.to/app-crud/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend using TypeScript and enable type-aware lint rules. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. 13 | -------------------------------------------------------------------------------- /dev.to/app-crud/data/users.db: -------------------------------------------------------------------------------- 1 | {"name":"nnnnn","email":"nnnnn","_id":"JVGGm6l1PpaKQqs6"} 2 | {"$$deleted":true,"_id":"JVGGm6l1PpaKQqs6"} 3 | {"name":"","email":"","_id":"RQ7j69Px6aO325lz"} 4 | {"name":"Willyams","email":"yracnet@gmail.com","_id":"OLY3sQAYeEVesSsa"} 5 | {"name":"Willyams","email":"yracnet@gmail.com","_id":"yFshoDP0E05GxPAK"} 6 | {"name":"Willyams","email":"yracnet@gmail.com","_id":"ezQsFmyQ3enmGGxg"} 7 | {"name":"Willyams","email":"yracnet@gmail.com","_id":"ioUPKTd3umclhKxs"} 8 | {"name":"Willyams","email":"yracnet@gmail.com","_id":"Eo6qBpDeBDF59kcN"} 9 | {"$$deleted":true,"_id":"RQ7j69Px6aO325lz"} 10 | {"$$deleted":true,"_id":"ezQsFmyQ3enmGGxg"} 11 | {"$$deleted":true,"_id":"ioUPKTd3umclhKxs"} 12 | {"$$deleted":true,"_id":"yFshoDP0E05GxPAK"} 13 | {"name":"demo","email":"demo@demo.com","_id":"1bqDAMruP2N9NRKI"} 14 | {"name":"demo","email":"demo@demo.com","_id":"VSCRqJY2E8Ret41c"} 15 | {"name":"demo","email":"demo@demo.com","_id":"1bqDAMruP2N9NRKI"} 16 | {"name":"demo","email":"demo@demo.com","_id":"1bqDAMruP2N9NRKI"} 17 | {"name":"demo","email":"demo@demo.com","_id":"1bqDAMruP2N9NRKI"} 18 | {"name":"demo","email":"demo@demo.com","_id":"1bqDAMruP2N9NRKI"} 19 | {"name":"demo","email":"demo@demo.com","_id":"1bqDAMruP2N9NRKI"} 20 | {"name":"demo4444","email":"demo@demo.com","_id":"1bqDAMruP2N9NRKI"} 21 | {"name":"","email":"","_id":"n4Mh8Nb6BUeHdeeX"} 22 | {"name":"","email":"","_id":"Y16YRm6lcdwMm0Gq"} 23 | {"$$deleted":true,"_id":"Y16YRm6lcdwMm0Gq"} 24 | {"$$deleted":true,"_id":"n4Mh8Nb6BUeHdeeX"} 25 | {"$$deleted":true,"_id":"VSCRqJY2E8Ret41c"} 26 | {"name":"","email":"","_id":"tuDG63zq9nVDsaVs"} 27 | {"name":"","email":"","_id":"tuDG63zq9nVDsaVs"} 28 | {"name":"","email":"","_id":"tuDG63zq9nVDsaVs"} 29 | {"name":"","email":"ddd","_id":"tuDG63zq9nVDsaVs"} 30 | {"name":"fff","email":"ddd","_id":"tuDG63zq9nVDsaVs"} 31 | {"name":"sss","email":"ss","_id":"jxmZqSeA5Jv5ANCS"} 32 | {"name":"sss","email":"ss","_id":"jxmZqSeA5Jv5ANCS"} 33 | {"name":"sss","email":"ss","_id":"jxmZqSeA5Jv5ANCS"} 34 | {"name":"sssdsfgsdfg","email":"ssdsfgsdfg","_id":"jxmZqSeA5Jv5ANCS"} 35 | -------------------------------------------------------------------------------- /dev.to/app-crud/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | 6 | export default [ 7 | { ignores: ['dist'] }, 8 | { 9 | files: ['**/*.{js,jsx}'], 10 | languageOptions: { 11 | ecmaVersion: 2020, 12 | globals: globals.browser, 13 | parserOptions: { 14 | ecmaVersion: 'latest', 15 | ecmaFeatures: { jsx: true }, 16 | sourceType: 'module', 17 | }, 18 | }, 19 | plugins: { 20 | 'react-hooks': reactHooks, 21 | 'react-refresh': reactRefresh, 22 | }, 23 | rules: { 24 | ...js.configs.recommended.rules, 25 | ...reactHooks.configs.recommended.rules, 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | 'react-refresh/only-export-components': [ 28 | 'warn', 29 | { allowConstantExport: true }, 30 | ], 31 | }, 32 | }, 33 | ] 34 | -------------------------------------------------------------------------------- /dev.to/app-crud/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CRUD Vite + React 8 | 9 | 10 |
11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /dev.to/app-crud/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-crud", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "express": "^4.21.2", 14 | "nedb": "^1.8.0", 15 | "react": "^19.0.0", 16 | "react-bootstrap": "^2.10.9", 17 | "react-dom": "^19.0.0", 18 | "vite-plugin-api-routes": "^1.2.1-beta" 19 | }, 20 | "devDependencies": { 21 | "@eslint/js": "^9.21.0", 22 | "@types/react": "^19.0.10", 23 | "@types/react-dom": "^19.0.4", 24 | "@vitejs/plugin-react-swc": "^3.8.0", 25 | "eslint": "^9.21.0", 26 | "eslint-plugin-react-hooks": "^5.1.0", 27 | "eslint-plugin-react-refresh": "^0.4.19", 28 | "globals": "^15.15.0", 29 | "vite": "^6.2.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dev.to/app-crud/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/client/api/dataProvider.js: -------------------------------------------------------------------------------- 1 | const NOP = (data) => data; 2 | 3 | const getQuery = (params = {}) => { 4 | return Object.entries(params) 5 | .reduce((acc, [name, value]) => { 6 | acc.push(`${name}=${value}`); 7 | return acc; 8 | }, []) 9 | .join("&"); 10 | }; 11 | 12 | export const createDataProvider = ({ 13 | url = "/myapp/api", 14 | resource = "", 15 | assertHeaders = NOP, 16 | assertOptions = NOP, 17 | } = {}) => { 18 | const getList = async (params = {}) => { 19 | const query = getQuery(params); 20 | const headers = assertHeaders({ 21 | Accept: "application/json", 22 | }); 23 | const options = assertOptions({ headers }); 24 | try { 25 | const response = await fetch(`${url}/${resource}?${query}`, options); 26 | const { data, error = "", message = "" } = await response.json(); 27 | return { data, error, message }; 28 | } catch (error) { 29 | return { error: error.message || "Error fetching the list" }; 30 | } 31 | }; 32 | const getOne = async (id, params = {}) => { 33 | const query = getQuery(params); 34 | const headers = assertHeaders({ 35 | Accept: "application/json", 36 | }); 37 | const options = assertOptions({ headers }); 38 | try { 39 | const response = await fetch( 40 | `${url}/${resource}/${id}?${query}`, 41 | options 42 | ); 43 | const { data, error = "", message = "" } = await response.json(); 44 | return { data, error, message }; 45 | } catch (error) { 46 | return { error: error.message || "Error fetching the resource" }; 47 | } 48 | }; 49 | const createOne = async (data) => { 50 | const headers = assertHeaders({ 51 | Accept: "application/json", 52 | "Content-Type": "application/json", 53 | }); 54 | const options = assertOptions({ 55 | headers, 56 | method: "POST", 57 | body: JSON.stringify(data), 58 | }); 59 | try { 60 | const response = await fetch(`${url}/${resource}`, options); 61 | const { data, error = "", message = "" } = await response.json(); 62 | return { data, error, message }; 63 | } catch (error) { 64 | return { error: error.message || "Error creating the resource" }; 65 | } 66 | }; 67 | 68 | const updateOne = async (id, data) => { 69 | const headers = assertHeaders({ 70 | Accept: "application/json", 71 | "Content-Type": "application/json", 72 | }); 73 | const options = assertOptions({ 74 | headers, 75 | method: "PUT", 76 | body: JSON.stringify(data), 77 | }); 78 | try { 79 | const response = await fetch(`${url}/${resource}/${id}`, options); 80 | const { data, error = "", message = "" } = await response.json(); 81 | return { data, error, message }; 82 | } catch (error) { 83 | return { error: error.message || "Error updating the resource" }; 84 | } 85 | }; 86 | 87 | const deleteOne = async (id) => { 88 | const headers = assertHeaders({ 89 | Accept: "application/json", 90 | }); 91 | const options = assertOptions({ 92 | headers, 93 | method: "DELETE", 94 | }); 95 | try { 96 | const response = await fetch(`${url}/${resource}/${id}`, options); 97 | const { data, error = "", message = "" } = await response.json(); 98 | return { data, error, message }; 99 | } catch (error) { 100 | return { error: error.message || "Error deleting the resource" }; 101 | } 102 | }; 103 | 104 | return { getList, getOne, createOne, updateOne, deleteOne }; 105 | }; 106 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/client/api/index.js: -------------------------------------------------------------------------------- 1 | import { createDataProvider } from "./dataProvider"; 2 | 3 | export const userDP = createDataProvider({ resource: "user" }); 4 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/client/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode, useEffect, useState } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { UserAdmin } from "./user-admin"; 4 | 5 | const Timer = () => { 6 | const [time, setTime] = useState(""); 7 | useEffect(() => { 8 | const update = () => { 9 | const d = new Date(); 10 | setTime(d.toISOString()); 11 | }; 12 | const id = setInterval(update, 1000); 13 | return () => { 14 | clearInterval(id); 15 | }; 16 | }, []); 17 | return

{time}

; 18 | }; 19 | 20 | createRoot(document.getElementById("root")).render( 21 | 22 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/client/user-admin/form.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yracnet/vite-plugin-api-routes/5a08c59cf275b2abed08bf21f1b504ca62c3b77e/dev.to/app-crud/src/client/user-admin/form.jsx -------------------------------------------------------------------------------- /dev.to/app-crud/src/client/user-admin/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from "react"; 2 | import { 3 | Alert, 4 | Button, 5 | ButtonGroup, 6 | Col, 7 | Container, 8 | Form, 9 | InputGroup, 10 | Row, 11 | Spinner, 12 | Table, 13 | } from "react-bootstrap"; 14 | import { userDP } from "../api"; 15 | 16 | const UserContext = createContext(); 17 | 18 | const UserProvider = ({ children }) => { 19 | const [selectedKey, setSelectedKey] = useState(null); 20 | const [shouldReload, setShouldReload] = useState(true); 21 | 22 | return ( 23 | 26 | {children} 27 | 28 | ); 29 | }; 30 | 31 | const useUserContext = () => useContext(UserContext); 32 | 33 | const UserTable = () => { 34 | const { shouldReload, setShouldReload, setSelectedKey } = useUserContext(); 35 | const [searchQuery, setSearchQuery] = useState(""); 36 | 37 | const [data, setData] = useState([]); 38 | const [error, setError] = useState(null); 39 | const [message, setMessage] = useState(null); 40 | 41 | const handleEdit = async (userId) => { 42 | setSelectedKey(userId); 43 | }; 44 | 45 | const handleDelete = async (userId) => { 46 | const { message, error } = await userDP.deleteOne(userId); 47 | setData([]); 48 | setError(error); 49 | setMessage(message); 50 | setSelectedKey(null); 51 | if (!error) { 52 | handleSearch(); 53 | } 54 | }; 55 | 56 | const handleSearch = async () => { 57 | const { 58 | data = [], 59 | message, 60 | error, 61 | } = await userDP.getList({ q: searchQuery }); 62 | setData(data); 63 | setError(error); 64 | setMessage(message); 65 | setShouldReload(false); 66 | }; 67 | 68 | useEffect(() => { 69 | if (shouldReload) { 70 | handleSearch(); 71 | } 72 | }, [shouldReload]); 73 | 74 | return ( 75 |
76 | 77 | setSearchQuery(e.target.value)} 82 | /> 83 | 84 | 85 | {shouldReload && } 86 | {error && {error}} 87 | {message && {message}} 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {data.map((user) => ( 98 | 99 | 100 | 101 | 118 | 119 | ))} 120 | 121 |
NombreEmailAcciones
{user.name}{user.email} 102 | 103 | 109 | 116 | 117 |
122 |
123 | ); 124 | }; 125 | 126 | const UserForm = () => { 127 | const { selectedKey, setSelectedKey, setShouldReload } = useUserContext(); 128 | 129 | const [user, setUser] = useState({ 130 | name: "", 131 | email: "", 132 | }); 133 | const [error, setError] = useState(null); 134 | const [message, setMessage] = useState(null); 135 | 136 | const handleSubmit = async (e) => { 137 | e.preventDefault(); 138 | const { data, error, message } = selectedKey 139 | ? await userDP.updateOne(user._id, user) 140 | : await userDP.createOne(user); 141 | setError(error); 142 | setMessage(message); 143 | setUser(data); 144 | setSelectedKey(data._id); 145 | setShouldReload(true); 146 | }; 147 | 148 | const handleReset = async () => { 149 | const { 150 | data = { 151 | name: "", 152 | email: "", 153 | }, 154 | message, 155 | error, 156 | } = selectedKey ? await userDP.getOne(selectedKey) : {}; 157 | setError(error); 158 | setMessage(message); 159 | setUser(data); 160 | }; 161 | 162 | const onChangeAttr = (name, value) => { 163 | setUser((prev) => { 164 | return { ...prev, [name]: value }; 165 | }); 166 | }; 167 | 168 | const handleNew = async () => { 169 | setError(""); 170 | setMessage(""); 171 | setUser({ 172 | name: "", 173 | email: "", 174 | }); 175 | setSelectedKey(null); 176 | }; 177 | 178 | useEffect(() => { 179 | handleReset(); 180 | }, [selectedKey]); 181 | 182 | return ( 183 |
184 | onChangeAttr("name", e.target.value)} 189 | /> 190 | onChangeAttr("email", e.target.value)} 195 | /> 196 | 197 | 205 | 208 | 211 | 212 | {error && {error}} 213 | {message && {message}} 214 |
215 | ); 216 | }; 217 | 218 | export const UserAdmin = () => { 219 | return ( 220 | 221 | 222 |

Gestión de Usuarios

223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 |
232 |
233 | ); 234 | }; 235 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/client/user-admin/table.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yracnet/vite-plugin-api-routes/5a08c59cf275b2abed08bf21f1b504ca62c3b77e/dev.to/app-crud/src/client/user-admin/table.jsx -------------------------------------------------------------------------------- /dev.to/app-crud/src/server/api-dev/GET.js: -------------------------------------------------------------------------------- 1 | const PING = (req, res, next) => { 2 | res.status(200).json({ env: "IN DEVELOPMENT" }); 3 | }; 4 | 5 | export default PING; 6 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/server/api-prod/GET.js: -------------------------------------------------------------------------------- 1 | const PING = (req, res, next) => { 2 | res.status(200).json({ env: "IN PRODUCION" }); 3 | }; 4 | 5 | export default PING; 6 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/server/api/AUTH.js: -------------------------------------------------------------------------------- 1 | export default (req, res, next) => { 2 | req.data = "AUTH HANDLER"; 3 | next(); 4 | }; 5 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/server/api/ERROR.js: -------------------------------------------------------------------------------- 1 | const ERROR = (error, _, res, next) => { 2 | if (error instanceof Error) { 3 | res 4 | .status(403) 5 | .json({ name: "ERROR HANDLER!!!!!!!!!", error: error.message }); 6 | } else { 7 | next(error); 8 | } 9 | }; 10 | export default ERROR; 11 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/server/api/user/GET.js: -------------------------------------------------------------------------------- 1 | // src/server/api/user/GET.js 2 | import db from "../../db"; 3 | 4 | const GET_USERS = (req, res, next) => { 5 | db.users.find({}, (err, users) => { 6 | if (err) { 7 | return res.status(500).json({ error: "Error fetching users" }); 8 | } 9 | res.status(200).json({ data: users, from: req.data }); 10 | }); 11 | }; 12 | 13 | export default GET_USERS; 14 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/server/api/user/POST.js: -------------------------------------------------------------------------------- 1 | // src/server/api/user/POST.js 2 | import db from "../../db"; 3 | 4 | const CREATE_USER = (req, res, next) => { 5 | const { name, email } = req.body; 6 | db.users.insert({ name, email }, (err, user) => { 7 | if (err) { 8 | return res.status(500).json({ error: "Error inserting user" }); 9 | } 10 | res.status(201).json({ 11 | data: user, 12 | message: "User successfully inserted", 13 | }); 14 | }); 15 | }; 16 | 17 | export default CREATE_USER; 18 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/server/api/user/[userId]/DELETE.js: -------------------------------------------------------------------------------- 1 | // src/server/api/user/[userId]/DELETE.js 2 | import db from "../../../db"; 3 | 4 | const DELETE_USER = (req, res, next) => { 5 | const { userId } = req.params; 6 | 7 | db.users.remove({ _id: userId }, {}, (err, numRemoved) => { 8 | if (err) { 9 | return res.status(500).json({ error: "Error deleting user" }); 10 | } 11 | if (numRemoved === 0) { 12 | return res.status(404).json({ error: "User not found" }); 13 | } 14 | res.status(200).json({ message: "User successfully deleted" }); 15 | }); 16 | }; 17 | 18 | export default DELETE_USER; 19 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/server/api/user/[userId]/GET.js: -------------------------------------------------------------------------------- 1 | // src/server/api/user/[userId]/GET.js 2 | import db from "../../../db"; 3 | 4 | const GET_USER = (req, res, next) => { 5 | const { userId } = req.params; 6 | 7 | db.users.findOne({ _id: userId }, (err, user) => { 8 | if (err) { 9 | return res.status(500).json({ error: "Error fetching user" }); 10 | } 11 | if (!user) { 12 | return res.status(404).json({ error: "User not found" }); 13 | } 14 | res.status(200).json({ data: user }); 15 | }); 16 | }; 17 | 18 | export default GET_USER; 19 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/server/api/user/[userId]/PUT.js: -------------------------------------------------------------------------------- 1 | // src/server/api/user/[userId]/PUT.js 2 | import db from "../../../db"; 3 | 4 | const UPDATE_USER = (req, res, next) => { 5 | const { userId } = req.params; 6 | const { name, email } = req.body; 7 | const data = { _id: userId, name, email }; 8 | db.users.update( 9 | { _id: userId }, 10 | { $set: { name, email } }, 11 | {}, 12 | (err, numReplaced) => { 13 | if (err) { 14 | return res.status(500).json({ data, error: "Error updating user" }); 15 | } 16 | if (numReplaced === 0) { 17 | return res.status(404).json({ data, error: "User not found" }); 18 | } 19 | res.status(200).json({ data, message: "User successfully updated" }); 20 | } 21 | ); 22 | }; 23 | 24 | export default UPDATE_USER; 25 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/server/configure.js: -------------------------------------------------------------------------------- 1 | // src/server/configure.js 2 | import express from "express"; 3 | 4 | // DEV MODE 5 | export const viteServerBefore = (server, viteServer) => { 6 | console.log("VITEJS SERVER"); 7 | server.use(express.json()); 8 | server.use(express.urlencoded({ extended: true })); 9 | }; 10 | 11 | // DEV MODE 12 | export const viteServerAfter = (server, viteServer) => { 13 | const errorHandler = (err, req, res, next) => { 14 | if (err instanceof Error) { 15 | res.writeHead(403, { "Content-Type": "application/json" }); 16 | res.end(JSON.stringify({ error: err.message })); 17 | } else { 18 | next(err); 19 | } 20 | }; 21 | server.use(errorHandler); 22 | }; 23 | 24 | // PROD MODE 25 | export const serverBefore = (server) => { 26 | server.use(express.json()); 27 | server.use(express.urlencoded({ extended: true })); 28 | }; 29 | 30 | // PROD MODE 31 | export const serverAfter = (server) => { 32 | const errorHandler = (err, req, res, next) => { 33 | if (err instanceof Error) { 34 | res.status(403).json({ error: err.message }); 35 | } else { 36 | next(err); 37 | } 38 | }; 39 | server.use(errorHandler); 40 | }; 41 | -------------------------------------------------------------------------------- /dev.to/app-crud/src/server/db/index.js: -------------------------------------------------------------------------------- 1 | // src/server/db/index.js 2 | import Datastore from "nedb"; 3 | 4 | export const users = new Datastore({ 5 | filename: "./data/users.db", 6 | autoload: true, 7 | }); 8 | 9 | const db = { users }; 10 | export default db; 11 | -------------------------------------------------------------------------------- /dev.to/app-crud/vite.config.js: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react-swc"; 2 | import { defineConfig } from "vite"; 3 | import apiRoutes from "vite-plugin-api-routes"; 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | base: "myapp", 8 | plugins: [ 9 | react(), 10 | apiRoutes({ 11 | mode: "isolated", 12 | configure: "src/server/configure.js", // Path to the configuration file 13 | mapper: { 14 | AUTH: { 15 | method: "use", 16 | priority: 11, 17 | }, 18 | ERROR: { 19 | method: "use", 20 | priority: 110, 21 | }, 22 | }, 23 | dirs: [ 24 | { 25 | dir: "src/server/api", // Path to the APIs 26 | route: "", 27 | }, 28 | //PROD AND DEV SWITCH 29 | { 30 | dir: "src/server/api-dev", 31 | route: "admin", 32 | skip: "production", 33 | }, 34 | { 35 | dir: "src/server/api-prod", 36 | route: "admin", 37 | skip: "development", 38 | }, 39 | ], 40 | }), 41 | ], 42 | }); 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspace-api-routes", 3 | "version": "1.0.0", 4 | "private": true, 5 | "repository": "https://github.com/yracnet/vite-plugin-api-routes.git", 6 | "author": "Willyams Yujra ", 7 | "license": "MIT", 8 | "workspaces": [ 9 | "packages/plugin", 10 | "packages/examples/app", 11 | "packages/examples/appIsolated", 12 | "packages/examples/dual", 13 | "packages/examples/basic" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/examples/app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/examples/app/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /packages/examples/app/data/products.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yracnet/vite-plugin-api-routes/5a08c59cf275b2abed08bf21f1b504ca62c3b77e/packages/examples/app/data/products.db -------------------------------------------------------------------------------- /packages/examples/app/data/users.db: -------------------------------------------------------------------------------- 1 | {"name":"Jesus","email":"jesus@gmail.com","_id":"B7qk33N3G2FnAp7F"} 2 | {"name":"Willyams","email":"yracnet@gmail.com","_id":"X6FbNfNQls6WGY3X"} 3 | {"name":"Omar","email":"omar@gmail.com","_id":"kiqUwaXZmXdG6pLP"} 4 | {"name":"Jesus 1","email":"jesus@gmail.com","_id":"B7qk33N3G2FnAp7F"} 5 | -------------------------------------------------------------------------------- /packages/examples/app/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /packages/examples/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/examples/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "data-provider": "^0.3.1", 14 | "nedb": "^1.8.0", 15 | "react": "^18.3.1", 16 | "react-bootstrap": "^2.10.7", 17 | "react-dom": "^18.3.1" 18 | }, 19 | "devDependencies": { 20 | "@eslint/js": "^9.17.0", 21 | "@types/react": "^18.3.18", 22 | "@types/react-dom": "^18.3.5", 23 | "@vitejs/plugin-react-swc": "^3.5.0", 24 | "eslint": "^9.17.0", 25 | "eslint-plugin-react": "^7.37.2", 26 | "eslint-plugin-react-hooks": "^5.0.0", 27 | "eslint-plugin-react-refresh": "^0.4.16", 28 | "globals": "^15.14.0", 29 | "vite": "^7.0.2", 30 | "vite-plugin-api-routes": "*", 31 | "vite-plugin-inspect": "^11.3.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/examples/app/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/examples/app/src/client/api/dataProvider.js: -------------------------------------------------------------------------------- 1 | const NOP = (data) => data; 2 | 3 | const getQuery = (params = {}) => { 4 | return Object.entries(params) 5 | .reduce((acc, [name, value]) => { 6 | acc.push(`${name}=${value}`); 7 | return acc; 8 | }, []) 9 | .join("&"); 10 | }; 11 | 12 | export const createDataProvider = ({ 13 | //options 14 | url = "/api", 15 | resource = "", 16 | assertHeaders = NOP, 17 | assertOptions = NOP, 18 | } = {}) => { 19 | const getList = async (params = {}) => { 20 | const query = getQuery(params); 21 | const headers = assertHeaders({ 22 | Accept: "application/json", 23 | }); 24 | const options = assertOptions({ headers }); 25 | try { 26 | const response = await fetch(`${url}/${resource}?${query}`, options); 27 | const { data, error = "", message = "" } = await response.json(); 28 | return { data, error, message }; 29 | } catch (error) { 30 | return { error: error.message || "Error al obtener la lista" }; 31 | } 32 | }; 33 | const getOne = async (id, params = {}) => { 34 | const query = getQuery(params); 35 | const headers = assertHeaders({ 36 | Accept: "application/json", 37 | }); 38 | const options = assertOptions({ headers }); 39 | try { 40 | const response = await fetch(`${url}/${resource}/${id}?${query}`, options); 41 | const { data, error = "", message = "" } = await response.json(); 42 | return { data, error, message }; 43 | } catch (error) { 44 | return { error: error.message || "Error al obtener el recurso" }; 45 | } 46 | }; 47 | const createOne = async (data) => { 48 | const headers = assertHeaders({ 49 | Accept: "application/json", 50 | "Content-Type": "application/json", 51 | }); 52 | const options = assertOptions({ headers, method: "POST", body: JSON.stringify(data) }); 53 | try { 54 | const response = await fetch(`${url}/${resource}`, options); 55 | const { data, error = "", message = "" } = await response.json(); 56 | return { data, error, message }; 57 | } catch (error) { 58 | return { error: error.message || "Error al crear el recurso" }; 59 | } 60 | }; 61 | const updateOne = async (id, data) => { 62 | const headers = assertHeaders({ 63 | Accept: "application/json", 64 | "Content-Type": "application/json", 65 | }); 66 | const options = assertOptions({ headers, method: "PUT", body: JSON.stringify(data) }); 67 | try { 68 | const response = await fetch(`${url}/${resource}/${id}`, options); 69 | const { data, error = "", message = "" } = await response.json(); 70 | return { data, error, message }; 71 | } catch (error) { 72 | return { error: error.message || "Error al actualizar el recurso" }; 73 | } 74 | }; 75 | const patchOne = async (id, data) => { 76 | const headers = assertHeaders({ 77 | Accept: "application/json", 78 | "Content-Type": "application/json", 79 | }); 80 | const options = assertOptions({ headers, method: "PATCH", body: JSON.stringify(data) }); 81 | try { 82 | const response = await fetch(`${url}/${resource}/${id}`, options); 83 | const { data, error = "", message = "" } = await response.json(); 84 | return { data, error, message }; 85 | } catch (error) { 86 | return { error: error.message || "Error al actualizar parcialmente el recurso" }; 87 | } 88 | }; 89 | const deleteOne = async (id) => { 90 | const headers = assertHeaders({ 91 | Accept: "application/json", 92 | }); 93 | const options = assertOptions({ headers, method: "DELETE" }); 94 | try { 95 | const response = await fetch(`${url}/${resource}/${id}`, options); 96 | const { data, error = "", message = "" } = await response.json(); 97 | return { data, error, message }; 98 | } catch (error) { 99 | return { error: error.message || "Error al eliminar el recurso" }; 100 | } 101 | }; 102 | return { 103 | getList, 104 | getOne, 105 | createOne, 106 | updateOne, 107 | patchOne, 108 | deleteOne, 109 | }; 110 | }; 111 | 112 | export const createDataProviderV2 = ({ 113 | //options 114 | url = "/api", 115 | resource = "", 116 | assertHeaders = NOP, 117 | assertOptions = NOP, 118 | } = {}) => { 119 | const getList = async (params = {}) => { 120 | const query = getQuery(params); 121 | const headers = assertHeaders({ 122 | Accept: "application/json", 123 | }); 124 | const options = assertOptions({ headers }); 125 | try { 126 | const response = await fetch(`${url}/${resource}?${query}`, options); 127 | const result = await response.json(); 128 | return { data: result.data, error: result.error, message: result.message }; 129 | } catch (error) { 130 | return { error: error.message || "Error al obtener la lista" }; 131 | } 132 | }; 133 | 134 | const getOne = async (id, params = {}) => { 135 | const query = getQuery(params); 136 | const headers = assertHeaders({ 137 | Accept: "application/json", 138 | }); 139 | const options = assertOptions({ headers }); 140 | try { 141 | const response = await fetch(`${url}/${resource}/${id}?${query}`, options); 142 | const result = await response.json(); 143 | return { data: result.data, error: result.error, message: result.message }; 144 | } catch (error) { 145 | return { error: error.message || "Error al obtener el recurso" }; 146 | } 147 | }; 148 | 149 | const create = async (data) => { 150 | const headers = assertHeaders({ 151 | Accept: "application/json", 152 | "Content-Type": "application/json", 153 | }); 154 | const options = assertOptions({ headers, method: "POST", body: JSON.stringify(data) }); 155 | try { 156 | const response = await fetch(`${url}/${resource}`, options); 157 | const result = await response.json(); 158 | return { data: result.data, error: result.error, message: result.message }; 159 | } catch (error) { 160 | return { error: error.message || "Error al crear el recurso" }; 161 | } 162 | }; 163 | 164 | const update = async (id, data) => { 165 | const headers = assertHeaders({ 166 | Accept: "application/json", 167 | "Content-Type": "application/json", 168 | }); 169 | const options = assertOptions({ headers, method: "PUT", body: JSON.stringify(data) }); 170 | try { 171 | const response = await fetch(`${url}/${resource}/${id}`, options); 172 | const result = await response.json(); 173 | return { data: result.data, error: result.error, message: result.message }; 174 | } catch (error) { 175 | return { error: error.message || "Error al actualizar el recurso" }; 176 | } 177 | }; 178 | 179 | const patch = async (id, data) => { 180 | const headers = assertHeaders({ 181 | Accept: "application/json", 182 | "Content-Type": "application/json", 183 | }); 184 | const options = assertOptions({ headers, method: "PATCH", body: JSON.stringify(data) }); 185 | try { 186 | const response = await fetch(`${url}/${resource}/${id}`, options); 187 | const result = await response.json(); 188 | return { data: result.data, error: result.error, message: result.message }; 189 | } catch (error) { 190 | return { error: error.message || "Error al actualizar parcialmente el recurso" }; 191 | } 192 | }; 193 | 194 | const deleteOne = async (id) => { 195 | const headers = assertHeaders({ 196 | Accept: "application/json", 197 | }); 198 | const options = assertOptions({ headers, method: "DELETE" }); 199 | try { 200 | const response = await fetch(`${url}/${resource}/${id}`, options); 201 | const result = await response.json(); 202 | return { error: result.error, message: result.message }; 203 | } catch (error) { 204 | return { error: error.message || "Error al eliminar el recurso" }; 205 | } 206 | }; 207 | 208 | const deleteMany = async (ids) => { 209 | const headers = assertHeaders({ 210 | Accept: "application/json", 211 | "Content-Type": "application/json", 212 | }); 213 | const options = assertOptions({ headers, method: "POST", body: JSON.stringify({ ids }) }); 214 | try { 215 | const response = await fetch(`${url}/${resource}/deleteMany`, options); 216 | const result = await response.json(); 217 | return { error: result.error, message: result.message }; 218 | } catch (error) { 219 | return { error: error.message || "Error al eliminar múltiples recursos" }; 220 | } 221 | }; 222 | 223 | const getMany = async (ids) => { 224 | const headers = assertHeaders({ 225 | Accept: "application/json", 226 | "Content-Type": "application/json", 227 | }); 228 | const options = assertOptions({ headers, method: "POST", body: JSON.stringify({ ids }) }); 229 | try { 230 | const response = await fetch(`${url}/${resource}/many`, options); 231 | const result = await response.json(); 232 | return { data: result.data, error: result.error, message: result.message }; 233 | } catch (error) { 234 | return { error: error.message || "Error al obtener múltiples recursos" }; 235 | } 236 | }; 237 | 238 | const getManyReference = async (target, id, params = {}) => { 239 | const query = getQuery(params); 240 | const headers = assertHeaders({ 241 | Accept: "application/json", 242 | }); 243 | const options = assertOptions({ headers }); 244 | try { 245 | const response = await fetch(`${url}/${resource}/${id}/${target}?${query}`, options); 246 | const result = await response.json(); 247 | return { data: result.data, error: result.error, message: result.message }; 248 | } catch (error) { 249 | return { error: error.message || "Error al obtener los recursos relacionados" }; 250 | } 251 | }; 252 | 253 | return { 254 | getList, 255 | getOne, 256 | create, 257 | update, 258 | patch, 259 | deleteOne, 260 | deleteMany, 261 | getMany, 262 | getManyReference, 263 | }; 264 | }; 265 | -------------------------------------------------------------------------------- /packages/examples/app/src/client/api/index.js: -------------------------------------------------------------------------------- 1 | import { createDataProvider } from "./dataProvider"; 2 | 3 | export const userDP = createDataProvider({ resource: "user" }); 4 | export const productDP = createDataProvider({ resource: "product" }); 5 | -------------------------------------------------------------------------------- /packages/examples/app/src/client/hooks/index.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | const NOP = async () => {}; 4 | export const useAPICallback = (callback = NOP, { dataInit = {}, errorInit = "", messageInit = "" } = {}) => { 5 | const [data, setData] = useState(dataInit); 6 | const [error, setError] = useState(errorInit); 7 | const [message, setMessage] = useState(messageInit); 8 | 9 | const onReload = async (...args) => { 10 | const { data, error = "", message = "" } = await callback(...args); 11 | setData(data); 12 | setError(error); 13 | setMessage(message); 14 | }; 15 | 16 | const setResult = ({ data, error = "", message = "" } = {}) => { 17 | setData(data); 18 | setError(error); 19 | setMessage(message); 20 | }; 21 | 22 | return { 23 | onReload, 24 | data, 25 | setData, 26 | error, 27 | setError, 28 | message, 29 | setMessage, 30 | setResult, 31 | }; 32 | }; 33 | 34 | const eventMap = new Map(); 35 | 36 | export const useEventDispatch = (defaultEvent = "all") => { 37 | const dispatchEvent = useCallback((data) => { 38 | const listeners = eventMap.get(data.event || defaultEvent); 39 | if (listeners) { 40 | listeners.forEach((listener) => listener(data)); 41 | } 42 | }, []); 43 | return dispatchEvent; 44 | }; 45 | 46 | export const useEventListener = (callback, event = "all") => { 47 | useEffect(() => { 48 | if (!eventMap.has(event)) { 49 | eventMap.set(event, []); 50 | } 51 | const listeners = eventMap.get(event); 52 | listeners.push(callback); 53 | return () => { 54 | const updatedListeners = listeners.filter((listener) => listener !== callback); 55 | eventMap.set(event, updatedListeners); 56 | }; 57 | }, [event, callback]); 58 | }; 59 | -------------------------------------------------------------------------------- /packages/examples/app/src/client/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode, useEffect, useState } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { UserMain } from "./user-admin"; 4 | 5 | const App = () => { 6 | const [time, setTime] = useState(""); 7 | useEffect(() => { 8 | const update = () => { 9 | const d = new Date(); 10 | setTime(d.toISOString()); 11 | }; 12 | const id = setInterval(update, 1000); 13 | return () => { 14 | clearInterval(id); 15 | }; 16 | }, []); 17 | return

{time}

; 18 | }; 19 | 20 | createRoot(document.getElementById("root")).render( 21 | 22 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /packages/examples/app/src/client/user-admin/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from "react"; 2 | import { Alert, Button, ButtonGroup, Col, Container, Form, InputGroup, Row, Spinner, Table } from "react-bootstrap"; 3 | import { userDP } from "../api"; 4 | 5 | const UserContext = createContext(); 6 | 7 | const UserProvider = ({ children }) => { 8 | const [selectedKey, setSelectedKey] = useState(null); 9 | const [shouldReload, setShouldReload] = useState(true); 10 | 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | const useUserContext = () => useContext(UserContext); 19 | 20 | const UserTable = () => { 21 | const { shouldReload, setShouldReload, setSelectedKey } = useUserContext(); 22 | const [searchQuery, setSearchQuery] = useState(""); 23 | 24 | const [data, setData] = useState([]); 25 | const [error, setError] = useState(null); 26 | const [message, setMessage] = useState(null); 27 | 28 | const handleEdit = async (userId) => { 29 | setSelectedKey(userId); 30 | }; 31 | 32 | const handleDelete = async (userId) => { 33 | const { message, error } = await userDP.deleteOne(userId); 34 | setData([]); 35 | setError(error); 36 | setMessage(message); 37 | setSelectedKey(null); 38 | if (!error) { 39 | handleSearch(); 40 | } 41 | }; 42 | 43 | const handleSearch = async () => { 44 | const { data, message, error } = await userDP.getList({ q: searchQuery }); 45 | setData(data); 46 | setError(error); 47 | setMessage(message); 48 | setShouldReload(false); 49 | }; 50 | 51 | useEffect(() => { 52 | if (shouldReload) { 53 | handleSearch(); 54 | } 55 | }, [shouldReload]); 56 | 57 | return ( 58 |
59 | 60 | setSearchQuery(e.target.value)} 65 | /> 66 | 67 | 68 | {shouldReload && } 69 | {error && {error}} 70 | {message && {message}} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {data.map((user) => ( 81 | 82 | 83 | 84 | 94 | 95 | ))} 96 | 97 |
NombreEmailAcciones
{user.name}{user.email} 85 | 86 | 89 | 92 | 93 |
98 |
99 | ); 100 | }; 101 | 102 | const UserForm = () => { 103 | const { selectedKey, setSelectedKey, setShouldReload } = useUserContext(); 104 | 105 | const [user, setUser] = useState({ 106 | name: "", 107 | email: "", 108 | }); 109 | const [error, setError] = useState(null); 110 | const [message, setMessage] = useState(null); 111 | 112 | const handleSubmit = async (e) => { 113 | e.preventDefault(); 114 | const { data, error, message } = selectedKey 115 | ? await userDP.updateOne(user._id, user) 116 | : await userDP.createOne(user); 117 | setError(error); 118 | setMessage(message); 119 | setUser(data); 120 | setShouldReload(true); 121 | }; 122 | 123 | const handleReset = async () => { 124 | const { 125 | data = { 126 | name: "", 127 | email: "", 128 | }, 129 | message, 130 | error, 131 | } = selectedKey ? await userDP.getOne(selectedKey) : {}; 132 | setError(error); 133 | setMessage(message); 134 | setUser(data); 135 | }; 136 | 137 | const onChangeAttr = (name, value) => { 138 | setUser((prev) => { 139 | return { ...prev, [name]: value }; 140 | }); 141 | }; 142 | 143 | const handleNew = async () => { 144 | setError(""); 145 | setMessage(""); 146 | setUser({ 147 | name: "", 148 | email: "", 149 | }); 150 | setSelectedKey(null); 151 | }; 152 | 153 | useEffect(() => { 154 | handleReset(); 155 | }, [selectedKey]); 156 | 157 | return ( 158 |
159 | onChangeAttr("name", e.target.value)} 164 | /> 165 | onChangeAttr("email", e.target.value)} 170 | /> 171 | 172 | 175 | 178 | 181 | 182 | {error && {error}} 183 | {message && {message}} 184 |
185 | ); 186 | }; 187 | 188 | export const UserMain = () => { 189 | return ( 190 | 191 | 192 |

Gestión de Usuarios

193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 |
202 |
203 | ); 204 | }; 205 | -------------------------------------------------------------------------------- /packages/examples/app/src/server/api/user/[id]/index.js: -------------------------------------------------------------------------------- 1 | import db from "../../../db"; 2 | 3 | export const GET = (req, res, next) => { 4 | const { id } = req.params; 5 | 6 | db.users.findOne({ _id: id }, (err, user) => { 7 | if (err) { 8 | return res.status(500).json({ error: "Error obteniendo el usuario" }); 9 | } 10 | if (!user) { 11 | return res.status(404).json({ error: "Usuario no encontrado" }); 12 | } 13 | res.status(200).json({ data: user }); 14 | }); 15 | }; 16 | 17 | export const DELETE = (req, res, next) => { 18 | const { id } = req.params; 19 | 20 | db.users.remove({ _id: id }, {}, (err, numRemoved) => { 21 | if (err) { 22 | return res.status(500).json({ error: "Error eliminando el usuario" }); 23 | } 24 | if (numRemoved === 0) { 25 | return res.status(404).json({ error: "Usuario no encontrado" }); 26 | } 27 | res.status(200).json({ message: "Usuario eliminado con éxito" }); 28 | }); 29 | }; 30 | 31 | export const PUT = (req, res, next) => { 32 | const { id } = req.params; 33 | const { name, email } = req.body; 34 | const data = { _id: id, name, email }; 35 | db.users.update({ _id: id }, { $set: { name, email } }, {}, (err, numReplaced) => { 36 | if (err) { 37 | return res.status(500).json({ data, error: "Error actualizando el usuario" }); 38 | } 39 | if (numReplaced === 0) { 40 | return res.status(404).json({ data, error: "Usuario no encontrado" }); 41 | } 42 | res.status(200).json({ data, message: "Usuario actualizado con éxito" }); 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/examples/app/src/server/api/user/index.js: -------------------------------------------------------------------------------- 1 | import db from "../../db"; 2 | 3 | export const GET = (req, res, next) => { 4 | db.users.find({}, (err, users) => { 5 | if (err) { 6 | return res.status(500).json({ error: "Error obteniendo los usuarios" }); 7 | } 8 | res.status(200).json({ data: users }); 9 | }); 10 | }; 11 | 12 | export const POST = (req, res, next) => { 13 | const { name, email } = req.body; 14 | db.users.insert({ name, email }, (err, user) => { 15 | if (err) { 16 | return res.status(500).json({ error: "Error al insertar el usuario" }); 17 | } 18 | res.status(201).json({ 19 | data: user, 20 | message: "Usuario insertado con éxito", 21 | }); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/examples/app/src/server/configure.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | export const viteServerBefore = (server, viteServer) => { 4 | console.log("VITEJS SERVER"); 5 | server.use(express.json()); 6 | server.use(express.urlencoded({ extended: true })); 7 | }; 8 | 9 | export const viteServerAfter = (server, viteServer) => { 10 | const errorHandler = (err, req, res, next) => { 11 | if (err instanceof Error) { 12 | res.writeHead(403, { "Content-Type": "application/json" }); 13 | res.end(JSON.stringify({ error: err.message })); 14 | } else { 15 | next(err); 16 | } 17 | }; 18 | server.use(errorHandler); 19 | }; 20 | 21 | // ServerHook 22 | export const serverBefore = (server) => { 23 | server.use(express.json()); 24 | server.use(express.urlencoded({ extended: true })); 25 | }; 26 | 27 | export const serverAfter = (server) => { 28 | const errorHandler = (err, req, res, next) => { 29 | if (err instanceof Error) { 30 | res.status(403).json({ error: err.message }); 31 | } else { 32 | next(err); 33 | } 34 | }; 35 | server.use(errorHandler); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/examples/app/src/server/db/index.js: -------------------------------------------------------------------------------- 1 | import Datastore from "nedb"; 2 | 3 | export const users = new Datastore({ filename: "./data/users.db", autoload: true }); 4 | export const products = new Datastore({ filename: "./data/products.db", autoload: true }); 5 | 6 | const db = { users, products }; 7 | export default db; 8 | -------------------------------------------------------------------------------- /packages/examples/app/vite.config.js: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react-swc"; 2 | import { defineConfig } from "vite"; 3 | //import apiRoutes from "vite-plugin-api-routes"; 4 | import inspect from "vite-plugin-inspect"; 5 | import apiRoutes from "../../plugin/src/index"; 6 | 7 | // https://vite.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | react(), 11 | inspect(), 12 | apiRoutes({ 13 | configure: "src/server/configure.js", 14 | mapper: { 15 | AUTH: { 16 | method: "use", 17 | priority: 10, 18 | }, 19 | ERROR: { 20 | method: "use", 21 | priority: 110, 22 | }, 23 | }, 24 | dirs: [ 25 | { 26 | dir: "src/server/api", 27 | route: "", 28 | }, 29 | ], 30 | }), 31 | ], 32 | }); 33 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/data/products.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yracnet/vite-plugin-api-routes/5a08c59cf275b2abed08bf21f1b504ca62c3b77e/packages/examples/appIsolated/data/products.db -------------------------------------------------------------------------------- /packages/examples/appIsolated/data/users.db: -------------------------------------------------------------------------------- 1 | {"name":"aaa","email":"aaa","_id":"7lIHliSsmk2t66f9"} 2 | {"name":"ddd","email":"ddd","_id":"CDVvstkprHTNZ1pO"} 3 | {"name":"ggg","email":"gg","_id":"FBU7HEmEZT7BBLFi"} 4 | {"name":"fff","email":"fff","_id":"VP6KdeihrQRQjYP5"} 5 | {"name":"Willyams","email":"yracnet@gmail.com","_id":"X6FbNfNQls6WGY3X"} 6 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appIsolated", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "data-provider": "^0.3.1", 14 | "nedb": "^1.8.0", 15 | "react": "^18.3.1", 16 | "react-bootstrap": "^2.10.7", 17 | "react-dom": "^18.3.1" 18 | }, 19 | "devDependencies": { 20 | "@eslint/js": "^9.17.0", 21 | "@types/react": "^18.3.18", 22 | "@types/react-dom": "^18.3.5", 23 | "@vitejs/plugin-react-swc": "^3.5.0", 24 | "eslint": "^9.17.0", 25 | "eslint-plugin-react": "^7.37.2", 26 | "eslint-plugin-react-hooks": "^5.0.0", 27 | "eslint-plugin-react-refresh": "^0.4.16", 28 | "globals": "^15.14.0", 29 | "vite": "^6.0.5", 30 | "vite-plugin-api-routes": "*" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/client/api/dataProvider.js: -------------------------------------------------------------------------------- 1 | const NOP = (data) => data; 2 | 3 | const getQuery = (params = {}) => { 4 | return Object.entries(params) 5 | .reduce((acc, [name, value]) => { 6 | acc.push(`${name}=${value}`); 7 | return acc; 8 | }, []) 9 | .join("&"); 10 | }; 11 | 12 | export const createDataProvider = ({ 13 | //options 14 | url = "/api", 15 | resource = "", 16 | assertHeaders = NOP, 17 | assertOptions = NOP, 18 | } = {}) => { 19 | const getList = async (params = {}) => { 20 | const query = getQuery(params); 21 | const headers = assertHeaders({ 22 | Accept: "application/json", 23 | }); 24 | const options = assertOptions({ headers }); 25 | try { 26 | const response = await fetch(`${url}/${resource}?${query}`, options); 27 | const { data, error = "", message = "" } = await response.json(); 28 | return { data, error, message }; 29 | } catch (error) { 30 | return { error: error.message || "Error al obtener la lista" }; 31 | } 32 | }; 33 | const getOne = async (id, params = {}) => { 34 | const query = getQuery(params); 35 | const headers = assertHeaders({ 36 | Accept: "application/json", 37 | }); 38 | const options = assertOptions({ headers }); 39 | try { 40 | const response = await fetch( 41 | `${url}/${resource}/${id}?${query}`, 42 | options 43 | ); 44 | const { data, error = "", message = "" } = await response.json(); 45 | return { data, error, message }; 46 | } catch (error) { 47 | return { error: error.message || "Error al obtener el recurso" }; 48 | } 49 | }; 50 | const createOne = async (data) => { 51 | const headers = assertHeaders({ 52 | Accept: "application/json", 53 | "Content-Type": "application/json", 54 | }); 55 | const options = assertOptions({ 56 | headers, 57 | method: "POST", 58 | body: JSON.stringify(data), 59 | }); 60 | try { 61 | const response = await fetch(`${url}/${resource}`, options); 62 | const { data, error = "", message = "" } = await response.json(); 63 | return { data, error, message }; 64 | } catch (error) { 65 | return { error: error.message || "Error al crear el recurso" }; 66 | } 67 | }; 68 | const updateOne = async (id, data) => { 69 | const headers = assertHeaders({ 70 | Accept: "application/json", 71 | "Content-Type": "application/json", 72 | }); 73 | const options = assertOptions({ 74 | headers, 75 | method: "PUT", 76 | body: JSON.stringify(data), 77 | }); 78 | try { 79 | const response = await fetch(`${url}/${resource}/${id}`, options); 80 | const { data, error = "", message = "" } = await response.json(); 81 | return { data, error, message }; 82 | } catch (error) { 83 | return { error: error.message || "Error al actualizar el recurso" }; 84 | } 85 | }; 86 | const patchOne = async (id, data) => { 87 | const headers = assertHeaders({ 88 | Accept: "application/json", 89 | "Content-Type": "application/json", 90 | }); 91 | const options = assertOptions({ 92 | headers, 93 | method: "PATCH", 94 | body: JSON.stringify(data), 95 | }); 96 | try { 97 | const response = await fetch(`${url}/${resource}/${id}`, options); 98 | const { data, error = "", message = "" } = await response.json(); 99 | return { data, error, message }; 100 | } catch (error) { 101 | return { 102 | error: error.message || "Error al actualizar parcialmente el recurso", 103 | }; 104 | } 105 | }; 106 | const deleteOne = async (id) => { 107 | const headers = assertHeaders({ 108 | Accept: "application/json", 109 | }); 110 | const options = assertOptions({ headers, method: "DELETE" }); 111 | try { 112 | const response = await fetch(`${url}/${resource}/${id}`, options); 113 | const { data, error = "", message = "" } = await response.json(); 114 | return { data, error, message }; 115 | } catch (error) { 116 | return { error: error.message || "Error al eliminar el recurso" }; 117 | } 118 | }; 119 | return { 120 | getList, 121 | getOne, 122 | createOne, 123 | updateOne, 124 | patchOne, 125 | deleteOne, 126 | }; 127 | }; 128 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/client/api/index.js: -------------------------------------------------------------------------------- 1 | import { createDataProvider } from "./dataProvider"; 2 | 3 | export const userDP = createDataProvider({ resource: "user" }); 4 | export const productDP = createDataProvider({ resource: "product" }); 5 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/client/hooks/index.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | const NOP = async () => {}; 4 | export const useAPICallback = (callback = NOP, { dataInit = {}, errorInit = "", messageInit = "" } = {}) => { 5 | const [data, setData] = useState(dataInit); 6 | const [error, setError] = useState(errorInit); 7 | const [message, setMessage] = useState(messageInit); 8 | 9 | const onReload = async (...args) => { 10 | const { data, error = "", message = "" } = await callback(...args); 11 | setData(data); 12 | setError(error); 13 | setMessage(message); 14 | }; 15 | 16 | const setResult = ({ data, error = "", message = "" } = {}) => { 17 | setData(data); 18 | setError(error); 19 | setMessage(message); 20 | }; 21 | 22 | return { 23 | onReload, 24 | data, 25 | setData, 26 | error, 27 | setError, 28 | message, 29 | setMessage, 30 | setResult, 31 | }; 32 | }; 33 | 34 | const eventMap = new Map(); 35 | 36 | export const useEventDispatch = (defaultEvent = "all") => { 37 | const dispatchEvent = useCallback((data) => { 38 | const listeners = eventMap.get(data.event || defaultEvent); 39 | if (listeners) { 40 | listeners.forEach((listener) => listener(data)); 41 | } 42 | }, []); 43 | return dispatchEvent; 44 | }; 45 | 46 | export const useEventListener = (callback, event = "all") => { 47 | useEffect(() => { 48 | if (!eventMap.has(event)) { 49 | eventMap.set(event, []); 50 | } 51 | const listeners = eventMap.get(event); 52 | listeners.push(callback); 53 | return () => { 54 | const updatedListeners = listeners.filter((listener) => listener !== callback); 55 | eventMap.set(event, updatedListeners); 56 | }; 57 | }, [event, callback]); 58 | }; 59 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/client/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode, useEffect, useState } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { UserMain } from "./user-admin"; 4 | 5 | const App = () => { 6 | const [time, setTime] = useState(""); 7 | useEffect(() => { 8 | const update = () => { 9 | const d = new Date(); 10 | setTime(d.toISOString()); 11 | }; 12 | const id = setInterval(update, 1000); 13 | return () => { 14 | clearInterval(id); 15 | }; 16 | }, []); 17 | return

{time}

; 18 | }; 19 | 20 | createRoot(document.getElementById("root")).render( 21 | 22 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/client/user-admin/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from "react"; 2 | import { Alert, Button, ButtonGroup, Col, Container, Form, InputGroup, Row, Spinner, Table } from "react-bootstrap"; 3 | import { userDP } from "../api"; 4 | 5 | const UserContext = createContext(); 6 | 7 | const UserProvider = ({ children }) => { 8 | const [selectedKey, setSelectedKey] = useState(null); 9 | const [shouldReload, setShouldReload] = useState(true); 10 | 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | const useUserContext = () => useContext(UserContext); 19 | 20 | const UserTable = () => { 21 | const { shouldReload, setShouldReload, setSelectedKey } = useUserContext(); 22 | const [searchQuery, setSearchQuery] = useState(""); 23 | 24 | const [data, setData] = useState([]); 25 | const [error, setError] = useState(null); 26 | const [message, setMessage] = useState(null); 27 | 28 | const handleEdit = async (userId) => { 29 | setSelectedKey(userId); 30 | }; 31 | 32 | const handleDelete = async (userId) => { 33 | const { message, error } = await userDP.deleteOne(userId); 34 | setData([]); 35 | setError(error); 36 | setMessage(message); 37 | setSelectedKey(null); 38 | if (!error) { 39 | handleSearch(); 40 | } 41 | }; 42 | 43 | const handleSearch = async () => { 44 | const { data, message, error } = await userDP.getList({ q: searchQuery }); 45 | setData(data); 46 | setError(error); 47 | setMessage(message); 48 | setShouldReload(false); 49 | }; 50 | 51 | useEffect(() => { 52 | if (shouldReload) { 53 | handleSearch(); 54 | } 55 | }, [shouldReload]); 56 | 57 | return ( 58 |
59 | 60 | setSearchQuery(e.target.value)} 65 | /> 66 | 67 | 68 | {shouldReload && } 69 | {error && {error}} 70 | {message && {message}} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {data.map((user) => ( 81 | 82 | 83 | 84 | 94 | 95 | ))} 96 | 97 |
NombreEmailAcciones
{user.name}{user.email} 85 | 86 | 89 | 92 | 93 |
98 |
99 | ); 100 | }; 101 | 102 | const UserForm = () => { 103 | const { selectedKey, setSelectedKey, setShouldReload } = useUserContext(); 104 | 105 | const [user, setUser] = useState({ 106 | name: "", 107 | email: "", 108 | }); 109 | const [error, setError] = useState(null); 110 | const [message, setMessage] = useState(null); 111 | 112 | const handleSubmit = async (e) => { 113 | e.preventDefault(); 114 | const { data, error, message } = selectedKey 115 | ? await userDP.updateOne(user._id, user) 116 | : await userDP.createOne(user); 117 | setError(error); 118 | setMessage(message); 119 | setUser(data); 120 | setShouldReload(true); 121 | }; 122 | 123 | const handleReset = async () => { 124 | const { 125 | data = { 126 | name: "", 127 | email: "", 128 | }, 129 | message, 130 | error, 131 | } = selectedKey ? await userDP.getOne(selectedKey) : {}; 132 | setError(error); 133 | setMessage(message); 134 | setUser(data); 135 | }; 136 | 137 | const onChangeAttr = (name, value) => { 138 | setUser((prev) => { 139 | return { ...prev, [name]: value }; 140 | }); 141 | }; 142 | 143 | const handleNew = async () => { 144 | setError(""); 145 | setMessage(""); 146 | setUser({ 147 | name: "", 148 | email: "", 149 | }); 150 | setSelectedKey(null); 151 | }; 152 | 153 | useEffect(() => { 154 | handleReset(); 155 | }, [selectedKey]); 156 | 157 | return ( 158 |
159 | onChangeAttr("name", e.target.value)} 164 | /> 165 | onChangeAttr("email", e.target.value)} 170 | /> 171 | 172 | 175 | 178 | 181 | 182 | {error && {error}} 183 | {message && {message}} 184 |
185 | ); 186 | }; 187 | 188 | export const UserMain = () => { 189 | return ( 190 | 191 | 192 |

Gestión de Usuarios

193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 |
202 |
203 | ); 204 | }; 205 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/server/api/user/GET.js: -------------------------------------------------------------------------------- 1 | import db from "../../db"; 2 | 3 | const GET_USERS = (req, res, next) => { 4 | console.log("LOG: GET_USERS"); 5 | db.users.find({}, (err, users) => { 6 | if (err) { 7 | return res.status(500).json({ error: "Error obteniendo los usuarios" }); 8 | } 9 | res.status(200).json({ data: users }); 10 | }); 11 | }; 12 | 13 | export default GET_USERS; 14 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/server/api/user/POST.js: -------------------------------------------------------------------------------- 1 | import db from "../../db"; 2 | 3 | const CREATE_USER = (req, res, next) => { 4 | const { name, email } = req.body; 5 | db.users.insert({ name, email }, (err, user) => { 6 | if (err) { 7 | return res.status(500).json({ error: "Error al insertar el usuario" }); 8 | } 9 | res.status(201).json({ 10 | data: user, 11 | message: "Usuario insertado con éxito", 12 | }); 13 | }); 14 | }; 15 | 16 | export default CREATE_USER; 17 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/server/api/user/USE.js: -------------------------------------------------------------------------------- 1 | export default (req, res, next) => { 2 | console.log("LOG: USE_USERS "); 3 | next(); 4 | }; 5 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/server/api/user/[userId]/DELETE.js: -------------------------------------------------------------------------------- 1 | import db from "../../../db"; 2 | 3 | const DELETE_USER = (req, res, next) => { 4 | const { userId } = req.params; 5 | 6 | db.users.remove({ _id: userId }, {}, (err, numRemoved) => { 7 | if (err) { 8 | return res.status(500).json({ error: "Error eliminando el usuario" }); 9 | } 10 | if (numRemoved === 0) { 11 | return res.status(404).json({ error: "Usuario no encontrado" }); 12 | } 13 | res.status(200).json({ message: "Usuario eliminado con éxito" }); 14 | }); 15 | }; 16 | 17 | export default DELETE_USER; 18 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/server/api/user/[userId]/GET.js: -------------------------------------------------------------------------------- 1 | import db from "../../../db"; 2 | 3 | const GET_USER = (req, res, next) => { 4 | const { userId } = req.params; 5 | 6 | db.users.findOne({ _id: userId }, (err, user) => { 7 | if (err) { 8 | return res.status(500).json({ error: "Error obteniendo el usuario" }); 9 | } 10 | if (!user) { 11 | return res.status(404).json({ error: "Usuario no encontrado" }); 12 | } 13 | res.status(200).json({ data: user }); 14 | }); 15 | }; 16 | export default GET_USER; 17 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/server/api/user/[userId]/PUT.js: -------------------------------------------------------------------------------- 1 | import db from "../../../db"; 2 | 3 | const UPDATE_USER = (req, res, next) => { 4 | const { userId } = req.params; 5 | const { name, email } = req.body; 6 | const data = { _id: userId, name, email }; 7 | db.users.update( 8 | { _id: userId }, 9 | { $set: { name, email } }, 10 | {}, 11 | (err, numReplaced) => { 12 | if (err) { 13 | return res 14 | .status(500) 15 | .json({ data, error: "Error actualizando el usuario" }); 16 | } 17 | if (numReplaced === 0) { 18 | return res.status(404).json({ data, error: "Usuario no encontrado" }); 19 | } 20 | res.status(200).json({ data, message: "Usuario actualizado con éxito" }); 21 | } 22 | ); 23 | }; 24 | export default UPDATE_USER; 25 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/server/api/user/[userId]/USE.js: -------------------------------------------------------------------------------- 1 | export default (req, res, next) => { 2 | const { userId } = req.params; 3 | console.log("LOG: USE_USER_BY_ID ", userId, req.originalUrl); 4 | next(); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/server/configure.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | export const viteServerBefore = (server, viteServer) => { 4 | console.log("VITEJS SERVER"); 5 | server.use(express.json()); 6 | server.use(express.urlencoded({ extended: true })); 7 | }; 8 | 9 | export const viteServerAfter = (server, viteServer) => { 10 | const errorHandler = (err, req, res, next) => { 11 | if (err instanceof Error) { 12 | res.writeHead(403, { "Content-Type": "application/json" }); 13 | res.end(JSON.stringify({ error: err.message })); 14 | } else { 15 | next(err); 16 | } 17 | }; 18 | server.use(errorHandler); 19 | }; 20 | 21 | // ServerHook 22 | export const serverBefore = (server) => { 23 | server.use(express.json()); 24 | server.use(express.urlencoded({ extended: true })); 25 | }; 26 | 27 | export const serverAfter = (server) => { 28 | const errorHandler = (err, req, res, next) => { 29 | if (err instanceof Error) { 30 | res.status(403).json({ error: err.message }); 31 | } else { 32 | next(err); 33 | } 34 | }; 35 | server.use(errorHandler); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/src/server/db/index.js: -------------------------------------------------------------------------------- 1 | import Datastore from "nedb"; 2 | 3 | export const users = new Datastore({ filename: "./data/users.db", autoload: true }); 4 | export const products = new Datastore({ filename: "./data/products.db", autoload: true }); 5 | 6 | const db = { users, products }; 7 | export default db; 8 | -------------------------------------------------------------------------------- /packages/examples/appIsolated/vite.config.js: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react-swc"; 2 | import { defineConfig } from "vite"; 3 | //import apiRoutes from "vite-plugin-api-routes"; 4 | import apiRoutes from "../../plugin/src/index"; 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | react(), 10 | apiRoutes({ 11 | mode: "isolated", 12 | configure: "src/server/configure.js", 13 | dirs: [ 14 | { 15 | dir: "src/server/api", 16 | route: "", 17 | }, 18 | ], 19 | }), 20 | ], 21 | }); 22 | -------------------------------------------------------------------------------- /packages/examples/basic/.env: -------------------------------------------------------------------------------- 1 | # JWT INSTANCE 2 | JWT_SECRET=F69F37EB4D1BAB673F7B5A7D41E3A 3 | JWT_EXPIRES=5m 4 | 5 | # SERVER DEPLOY 6 | SERVER_HOST=127.0.0.1 7 | SERVER_PORT=4000 -------------------------------------------------------------------------------- /packages/examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .api 27 | *.tgz 28 | .yarn 29 | .up-force 30 | .fetch-client -------------------------------------------------------------------------------- /packages/examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | VITE-PLUGIN-REST-API ( Vite + React + TS + EXPRESS) 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build" 9 | }, 10 | "dependencies": { 11 | "cookie-parser": "^1.4.7", 12 | "dotenv": "^16.4.7", 13 | "dotenv-local": "^1.0.1", 14 | "express": "^4.21.2", 15 | "jsonwebtoken": "^9.0.2", 16 | "react": "^18.0.0", 17 | "react-dom": "^18.0.0" 18 | }, 19 | "devDependencies": { 20 | "@types/express": "^5.0.0", 21 | "@types/node": "^22.10.5", 22 | "@types/react": "^19.0.2", 23 | "@types/react-dom": "^19.0.2", 24 | "@vitejs/plugin-react-swc": "^3.7.2", 25 | "typescript": "^5.7.2", 26 | "vite": "^6.0.5", 27 | "vite-plugin-api-routes": "*", 28 | "vite-plugin-chunk-split": "^0.5.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/examples/basic/private/.env: -------------------------------------------------------------------------------- 1 | # JWT INSTANCE 2 | JWT_SECRET=X7G9-H5Q2-TA4P-Z8B1 3 | JWT_EXPIRES=1m 4 | 5 | # SERVER DEPLOY 6 | SERVER_HOST=127.0.0.1 7 | SERVER_PORT=4000 -------------------------------------------------------------------------------- /packages/examples/basic/private/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "node app.js" 8 | }, 9 | "dependencies": { 10 | "cookie-parser": "^1.4.7", 11 | "dotenv": "^16.4.7", 12 | "dotenv-local": "^1.0.1", 13 | "express": "^4.21.2", 14 | "jsonwebtoken": "^9.0.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/examples/basic/private/readme.md: -------------------------------------------------------------------------------- 1 | # Sandbox Project 2 | 3 | This is a sandbox project for testing and development. Below are the steps to install and run the project, as well as the required environment variables. 4 | 5 | ## Installation 6 | 7 | To install the project dependencies, run the following command: 8 | 9 | ``` 10 | yarn install 11 | ``` 12 | 13 | ## Running the Project 14 | 15 | Once the dependencies are installed, you can start the project with: 16 | 17 | ``` 18 | yarn start 19 | ``` 20 | 21 | This will start the development server. 22 | 23 | ## Environment Variables 24 | 25 | Make sure to have a `.env` file with the following environment variables configured: 26 | 27 | ``` 28 | # JWT INSTANCE 29 | JWT_SECRET=F69F37EB4D1BAB673F7B5A7D41E3A 30 | JWT_EXPIRES=5m 31 | 32 | # SERVER DEPLOY 33 | SERVER_HOST=127.0.0.1 34 | SERVER_PORT=4000 35 | ``` 36 | 37 | ### Variable Descriptions 38 | 39 | - **JWT_SECRET**: Secret key used to sign JWT tokens. 40 | - **JWT_EXPIRES**: Expiration time for the JWT. 41 | - **SERVER_HOST**: The IP address of the server. 42 | - **SERVER_PORT**: The port on which the server will listen. 43 | -------------------------------------------------------------------------------- /packages/examples/basic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yracnet/vite-plugin-api-routes/5a08c59cf275b2abed08bf21f1b504ca62c3b77e/packages/examples/basic/public/favicon.ico -------------------------------------------------------------------------------- /packages/examples/basic/src/api/admin.ts: -------------------------------------------------------------------------------- 1 | import { createAllowRoles, assertToken } from "../middleware/sessionToken"; 2 | 3 | export default [ 4 | assertToken, 5 | createAllowRoles(['ADMIN']), 6 | ]; -------------------------------------------------------------------------------- /packages/examples/basic/src/api/admin/post/[postId]/index.ts: -------------------------------------------------------------------------------- 1 | import { createResponse } from "../../../../common/response"; 2 | 3 | //@ts-ignore 4 | export const GET = (req, res, next) => { 5 | createResponse("v2/post/[postId].ts", req, res, next); 6 | }; 7 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/admin/post/index.ts: -------------------------------------------------------------------------------- 1 | import { createResponse } from "../../../common/response"; 2 | 3 | //@ts-ignore 4 | export const GET = (req, res, next) => { 5 | createResponse("v2/post/index.ts", req, res, next); 6 | }; 7 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/admin/user.ts: -------------------------------------------------------------------------------- 1 | import { createAllowRoles } from "../../middleware/sessionToken"; 2 | 3 | export default createAllowRoles(['USER']); 4 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/admin/user/[userId]/index.ts: -------------------------------------------------------------------------------- 1 | import { createResponse } from "../../../../common/response"; 2 | 3 | //@ts-ignore 4 | export const GET = (req, res, next) => { 5 | createResponse("v2/user/[userId].ts", req, res, next); 6 | }; 7 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/admin/user/index.ts: -------------------------------------------------------------------------------- 1 | import { createResponse } from "../../../common/response"; 2 | 3 | //@ts-ignore 4 | export const GET = (req, res, next) => { 5 | createResponse("v2/user/index.ts", req, res, next); 6 | }; 7 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/auth/login.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { createResponseAsync } from "../../common/response"; 3 | import { jwtSign } from "../../middleware/jwt"; 4 | 5 | export const POST = (req: Request, res: Response, next: NextFunction) => { 6 | const token = jwtSign({ 7 | name: 'Willyams', 8 | roles: ['ADMIN', 'USER'], 9 | }); 10 | res.cookie('token', token, { httpOnly: false }); 11 | createResponseAsync("Logged in!", req, res, next); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/auth/logout.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { createResponseAsync } from "../../common/response"; 3 | 4 | export const POST = async (req: Request, res: Response, next: NextFunction) => { 5 | ["auth", "article", "user", "token"].forEach((name) => { 6 | res.clearCookie(name); 7 | }); 8 | return createResponseAsync("Logged out!", req, res, next); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/auth/supplant.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { createResponseAsync } from "../../common/response"; 3 | import { jwtSign } from "../../middleware/jwt"; 4 | 5 | //@ts-ignore 6 | export const POST = (req, res, next) => { 7 | const token = jwtSign({ 8 | name: "User Site", 9 | roles: ["SITE"], 10 | }); 11 | res.cookie("token", token, { httpOnly: false }); 12 | return createResponseAsync("Changed in!", req, res, next); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/routers.ts: -------------------------------------------------------------------------------- 1 | import { routers } from "@api/routers"; 2 | import { NextFunction, Request, Response } from "express"; 3 | 4 | export const GET = (req: Request, res: Response, next: NextFunction) => { 5 | const routesKeys = routers 6 | .filter((it) => it.path !== "/api/routers") 7 | .map((it, ix) => { 8 | //@ts-ignore 9 | it.key = `r${ix}`; 10 | return it; 11 | }) 12 | .sort((a, b) => { 13 | if (a.path.startsWith("/api/auth/")) { 14 | return -1; 15 | } 16 | if (b.path.startsWith("/api/auth/")) { 17 | return 0; 18 | } 19 | return 0; 20 | }); 21 | res.send(routesKeys); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/site.ts: -------------------------------------------------------------------------------- 1 | import { createAllowRoles, assertToken } from "../middleware/sessionToken"; 2 | 3 | export default [ 4 | assertToken, 5 | createAllowRoles(['SITE']), 6 | ]; -------------------------------------------------------------------------------- /packages/examples/basic/src/api/site/article/$articleId.js: -------------------------------------------------------------------------------- 1 | import { createResponse } from "../../../common/response"; 2 | 3 | export const GET = (req, res, next) => { 4 | createResponse("ARTICLE", req, res, next); 5 | }; 6 | export const DELETE = (req, res, next) => { 7 | createResponse("ARTICLE", req, res, next); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/site/article/new.js: -------------------------------------------------------------------------------- 1 | import { createResponse } from "../../../common/response"; 2 | const order = 0; 3 | export const GET = (req, res, next) => { 4 | createResponse("NEW ARTICLE", req, res, next); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/site/article/zip.js: -------------------------------------------------------------------------------- 1 | import { createResponse } from "../../../common/response"; 2 | const order = 0; 3 | export const GET = (req, res, next) => { 4 | createResponse("ZIP ARTICLE", req, res, next); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/site/index.js: -------------------------------------------------------------------------------- 1 | import { createResponse } from "../../common/response"; 2 | 3 | export const GET = (req, res, next) => { 4 | createResponse("PING EXAMPLE", req, res, next); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/examples/basic/src/api/site/page/$pageId.js: -------------------------------------------------------------------------------- 1 | import { createResponse } from "../../../common/response"; 2 | 3 | export const GET = (req, res, next) => { 4 | const { pageId } = req.params; 5 | createResponse("GET PAGE: " + pageId, req, res, next); 6 | }; 7 | -------------------------------------------------------------------------------- /packages/examples/basic/src/common/authMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | export function authMiddleware(req: Request, res: Response, next: NextFunction) { 4 | // Example authentication, please never it like this. 5 | if (req.headers['cookie']?.includes("auth=true")) { 6 | // User is authenticated 7 | return next(); 8 | } else { 9 | // User authentication failed, provide some details 10 | res.status(403).json({ 11 | message: "Invalid Login", 12 | cookies: req.headers['cookie'] ?? null /* For debug purposes */, 13 | _info: "Use the '/login' route first to get through protected routes" 14 | }) 15 | } 16 | } -------------------------------------------------------------------------------- /packages/examples/basic/src/common/response.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | export const createResponse = ( 4 | message: string, 5 | req: Request, 6 | res: Response, 7 | next: NextFunction 8 | ) => { 9 | res.send({ 10 | source: "sync", 11 | version: req.version, 12 | copyright: req.copyright, 13 | message, 14 | method: req.method, 15 | path: req.path, 16 | params: req.params, 17 | }); 18 | }; 19 | 20 | export const createResponseAsync = async ( 21 | message: string, 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ) => { 26 | return res.send({ 27 | source: "async", 28 | version: req.version, 29 | copyright: req.copyright, 30 | message, 31 | method: req.method, 32 | path: req.path, 33 | params: req.params, 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/examples/basic/src/custom-server-example/configure.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandleFunction } from "connect"; 2 | import express, { ErrorRequestHandler } from "express"; 3 | import { 4 | CallbackHook, 5 | ServerHook, 6 | ViteServerHook, 7 | } from "vite-plugin-api-routes/configure"; 8 | //@ts-ignore 9 | import cookieParser from "cookie-parser"; 10 | 11 | // ViteServerHook 12 | export const viteServerBefore: ViteServerHook = (server, viteServer) => { 13 | console.log("VITEJS SERVER"); 14 | server.use(express.json()); 15 | server.use(express.urlencoded({ extended: true })); 16 | server.use(cookieParser()); 17 | }; 18 | 19 | export const viteServerAfter: ViteServerHook = (server, viteServer) => { 20 | const errorHandler: ErrorHandleFunction = (err, req, res, next) => { 21 | if (err instanceof Error) { 22 | res.writeHead(403, { "Content-Type": "application/json" }); 23 | res.end(JSON.stringify({ error: err.message })); 24 | } else { 25 | next(err); 26 | } 27 | }; 28 | server.use(errorHandler); 29 | }; 30 | 31 | // ServerHook 32 | export const serverBefore: ServerHook = (server) => { 33 | server.use(express.json()); 34 | server.use(express.urlencoded({ extended: true })); 35 | server.use(cookieParser()); 36 | }; 37 | 38 | export const serverAfter: ServerHook = (server) => { 39 | const errorHandler: ErrorRequestHandler = (err, req, res, next) => { 40 | if (err instanceof Error) { 41 | res.status(403).json({ error: err.message }); 42 | } else { 43 | next(err); 44 | } 45 | }; 46 | server.use(errorHandler); 47 | }; 48 | 49 | // // HandlerHook 50 | // export const handlerBefore = (handler) => { 51 | // handler.use(express.json()); 52 | // handler.use(express.urlencoded({ extended: true })); 53 | // }; 54 | // export const handlerAfter = (server) => { 55 | // }; 56 | 57 | // CallbackHook 58 | export const callbackBefore: CallbackHook = (callback, route) => { 59 | // const { path } = route; 60 | // const { allowRoles } = callback; 61 | // if (allowRoles) { 62 | // const allowRolesFilter = createAllowRoles(allowRoles); 63 | // return [allowRolesFilter, callback]; 64 | // } 65 | return callback; 66 | }; 67 | 68 | // // StatusHook 69 | // export const serverListening = (server) => { 70 | // }; 71 | // export const serverError = (server, error) => { 72 | // }; 73 | -------------------------------------------------------------------------------- /packages/examples/basic/src/custom-server-example/handler.ts: -------------------------------------------------------------------------------- 1 | import * as configure from "@api/configure"; 2 | import { applyRouters } from "@api/routers"; 3 | import express from "express"; 4 | 5 | export const handler: any = express(); 6 | 7 | configure.handlerBefore?.(handler); 8 | 9 | applyRouters((module) => { 10 | const { method, route, cb } = module; 11 | if (handler[method]) { 12 | if (Array.isArray(cb)) { 13 | handler[method](route, ...cb); 14 | } else { 15 | handler[method](route, cb); 16 | } 17 | } else { 18 | console.log("Not Support", method, "for", route, "in", handler); 19 | } 20 | }); 21 | 22 | configure.handlerAfter?.(handler); 23 | -------------------------------------------------------------------------------- /packages/examples/basic/src/custom-server-example/server.ts: -------------------------------------------------------------------------------- 1 | import * as configure from "@api/configure"; 2 | import { handler } from "@api/handler"; 3 | import { endpoints } from "@api/routers"; 4 | import { loadEnv } from "dotenv-local"; 5 | import express from "express"; 6 | 7 | const server = express(); 8 | configure.serverBefore?.(server); 9 | 10 | const { HOST, PORT } = loadEnv({ 11 | envPrefix: "SERVER_", 12 | removeEnvPrefix: true, 13 | envInitial: { 14 | SERVER_HOST: "0.0.0.0", 15 | SERVER_PORT: "3000", 16 | }, 17 | }); 18 | 19 | const SERVER_URL = `http://${HOST}:${PORT}${API_ROUTES.BASE}`; 20 | 21 | server.use(API_ROUTES.BASE_API, handler); 22 | server.use(API_ROUTES.BASE, express.static(API_ROUTES.PUBLIC_DIR)); 23 | 24 | configure.serverAfter?.(server); 25 | 26 | const PORT_NRO = parseInt(PORT); 27 | server 28 | .listen(PORT_NRO, HOST, () => { 29 | console.log(`Ready at ${SERVER_URL}`); 30 | configure.serverListening?.(server, endpoints); 31 | }) 32 | .on("error", (error) => { 33 | console.error(`Error at ${SERVER_URL}`, error); 34 | configure.serverError?.(server, error); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/examples/basic/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { AppClient } from "./web/App.tsx"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /packages/examples/basic/src/middleware/jwt.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import jwt from 'jsonwebtoken'; 3 | import { loadEnv } from "dotenv-local"; 4 | 5 | const JWT = loadEnv({ 6 | envPrefix: 'JWT_', 7 | removeEnvPrefix: true, 8 | envInitial: { 9 | JWT_SECRET: '', 10 | JWT_EXPIRES: '30m' 11 | } 12 | }); 13 | 14 | export const jwtSign = (data: any) => jwt.sign(data, JWT.SECRET, { expiresIn: JWT.EXPIRES }); 15 | 16 | export const jwtVerify = (token: string = '') => jwt.verify(token, JWT.SECRET); 17 | 18 | export const jwtDecode = (token: string = '') => jwt.decode(token, {}); 19 | 20 | //export const JsonWebTokenError = jwt.JsonWebTokenError; 21 | //export const NotBeforeError = jwt.NotBeforeError; 22 | //export const TokenExpiredError = jwt.TokenExpiredError; 23 | -------------------------------------------------------------------------------- /packages/examples/basic/src/middleware/sessionToken.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { jwtVerify } from "./jwt"; 3 | 4 | export const assertToken = async (req: Request, res: Response, next: NextFunction) => { 5 | try { 6 | let token = req.query.token; 7 | if (!token) { 8 | token = req.headers['token']; 9 | } 10 | if (!token) { 11 | token = req.cookies?.token; 12 | } 13 | if (!token) { 14 | throw Error("Token required"); 15 | } 16 | const session = await jwtVerify(token); 17 | req.session = session; 18 | req.roles = session.roles; 19 | next(); 20 | } catch (error) { 21 | next(error); 22 | } 23 | } 24 | 25 | export const createAllowRoles = (allowRoles: string[]) => { 26 | return (req: Request, res: Response, next: NextFunction) => { 27 | try { 28 | const sessionRoles = req.roles || []; 29 | const ok = allowRoles.every(role => sessionRoles.includes(role)); 30 | if (!ok) { 31 | throw Error(`Not Allowed - Role required: ${allowRoles.join(',')}`); 32 | } 33 | next(); 34 | } catch (error) { 35 | next(error); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/examples/basic/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare namespace Express { 5 | export interface Request { 6 | session?: any; 7 | roles?: string[]; 8 | version?: string; 9 | copyright?: string; 10 | } 11 | } 12 | 13 | declare var API_ROUTES: any; 14 | -------------------------------------------------------------------------------- /packages/examples/basic/src/web/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | type JWTCookie = { 4 | jwt: any; 5 | cookie: any[]; 6 | }; 7 | const useJWTCookie = (): [JWTCookie, () => void] => { 8 | const [jwtCookie, setJWTCookie] = useState({ 9 | jwt: {}, 10 | cookie: [], 11 | }); 12 | useEffect(() => { 13 | const cookie = document.cookie.split(";").map((it) => { 14 | const [name, value] = it.split("="); 15 | return { 16 | name, 17 | value, 18 | }; 19 | }); 20 | let jwt: any = {}; 21 | try { 22 | const [, base64Url]: any = cookie 23 | .find((it) => it.name === "token") 24 | ?.value?.split("."); 25 | const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); 26 | const jsonPayload = decodeURIComponent( 27 | atob(base64) 28 | .split("") 29 | .map(function (c) { 30 | return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); 31 | }) 32 | .join("") 33 | ); 34 | jwt = JSON.parse(jsonPayload); 35 | } catch (error) { 36 | jwt = error; 37 | } 38 | setJWTCookie({ jwt, cookie }); 39 | }, [document.cookie]); 40 | const cleanCookies = () => { 41 | const cookies = document.cookie.split(";"); 42 | for (let i = 0; i < cookies.length; i++) { 43 | const cookie = cookies[i]; 44 | const eqPos = cookie.indexOf("="); 45 | const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; 46 | document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`; 47 | } 48 | setJWTCookie({ 49 | jwt: {}, 50 | cookie: [], 51 | }); 52 | }; 53 | return [jwtCookie, cleanCookies]; 54 | }; 55 | 56 | export const AppClient = () => { 57 | const [routers, setRouters] = useState([]); 58 | const [data, setData] = useState(""); 59 | const [{ jwt, cookie }, cleanCookies] = useJWTCookie(); 60 | 61 | const onLoad = async () => { 62 | let routers = await fetch(`${import.meta.env.BASE_URL}/api/routers`).then( 63 | (r) => r.json() 64 | ); 65 | setRouters(routers); 66 | }; 67 | 68 | const onTest = async (it: any) => { 69 | //@ts-ignore 70 | const id = parseInt(1000000 * Math.random()); 71 | const url = it.url.replace(/(:\w+)/g, id); 72 | let data = await fetch(url, { 73 | method: it.method, 74 | headers: { 75 | "HTTP-Action": it.action, 76 | }, 77 | }) 78 | .then((r) => r.json()) 79 | .catch((error) => ({ error, message: error.message })); 80 | setData(JSON.stringify(data, null, 2)); 81 | }; 82 | 83 | useEffect(() => { 84 | onLoad(); 85 | }, []); 86 | return ( 87 |
88 |
89 |
90 | VITE-PLUGIN-API-ROUTES 91 |
92 |
93 | 96 | 97 | 98 | 99 | 100 | 101 | {cookie.map((it, ix) => ( 102 | 103 | 104 | 105 | 106 | ))} 107 | 108 | 109 | 112 | 113 |
namevalue
{it.name}{it.value}
JWT 110 | {JSON.stringify(jwt, null, 2)} 111 |
114 | 115 |
116 |
117 | 118 | Reload 119 | 120 | 121 | 122 | {routers 123 | .filter((it) => it.method != "use") 124 | .map((it) => ( 125 | 126 | 127 | 128 | 136 | 137 | ))} 138 | 139 |
{it.method}{it.url} 129 | 135 |
140 |
141 |
142 | Response 143 |
144 |                 {data}
145 |               
146 |
147 |
148 |
149 |
150 |
151 | ); 152 | }; 153 | -------------------------------------------------------------------------------- /packages/examples/basic/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/examples/basic/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/examples/basic/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react-swc"; 2 | import { defineConfig } from "vite"; 3 | import { chunkSplitPlugin } from "vite-plugin-chunk-split"; 4 | import api from "../../plugin/src"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | base: "myapp", 9 | build: { 10 | minify: false, 11 | outDir: "dist/public", 12 | }, 13 | plugins: [ 14 | react(), 15 | //@ts-ignore 16 | api({ 17 | disableBuild: false, 18 | server: "src/custom-server-example/server.ts", 19 | //handler: "src/custom-server-example/handler.ts", 20 | configure: "src/custom-server-example/configure.ts", 21 | serverBuild: (config) => { 22 | config.plugins = [ 23 | //@ts-ignore 24 | chunkSplitPlugin({ 25 | strategy: "unbundle", 26 | customChunk: (args) => { 27 | const { file } = args; 28 | if (file.startsWith("src/")) { 29 | return file.replace("src/", ""); 30 | } 31 | if (file.startsWith(".api/")) { 32 | return file.replace(".api/", ""); 33 | } 34 | }, 35 | }), 36 | ]; 37 | return config; 38 | }, 39 | }), 40 | ], 41 | }); 42 | -------------------------------------------------------------------------------- /packages/examples/dual/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .api1 26 | .api2 27 | .api-legacy 28 | .api-isolated -------------------------------------------------------------------------------- /packages/examples/dual/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 13 | 14 | ```js 15 | export default tseslint.config({ 16 | extends: [ 17 | // Remove ...tseslint.configs.recommended and replace with this 18 | ...tseslint.configs.recommendedTypeChecked, 19 | // Alternatively, use this for stricter rules 20 | ...tseslint.configs.strictTypeChecked, 21 | // Optionally, add this for stylistic rules 22 | ...tseslint.configs.stylisticTypeChecked, 23 | ], 24 | languageOptions: { 25 | // other options... 26 | parserOptions: { 27 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 28 | tsconfigRootDir: import.meta.dirname, 29 | }, 30 | }, 31 | }) 32 | ``` 33 | 34 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 35 | 36 | ```js 37 | // eslint.config.js 38 | import reactX from 'eslint-plugin-react-x' 39 | import reactDom from 'eslint-plugin-react-dom' 40 | 41 | export default tseslint.config({ 42 | plugins: { 43 | // Add the react-x and react-dom plugins 44 | 'react-x': reactX, 45 | 'react-dom': reactDom, 46 | }, 47 | rules: { 48 | // other rules... 49 | // Enable its recommended typescript rules 50 | ...reactX.configs['recommended-typescript'].rules, 51 | ...reactDom.configs.recommended.rules, 52 | }, 53 | }) 54 | ``` 55 | -------------------------------------------------------------------------------- /packages/examples/dual/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /packages/examples/dual/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/examples/dual/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dual", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^19.0.0", 14 | "react-dom": "^19.0.0" 15 | }, 16 | "devDependencies": { 17 | "@eslint/js": "^9.21.0", 18 | "@types/react": "^19.0.10", 19 | "@types/react-dom": "^19.0.4", 20 | "@vitejs/plugin-react-swc": "^3.8.0", 21 | "eslint": "^9.21.0", 22 | "eslint-plugin-react-hooks": "^5.1.0", 23 | "eslint-plugin-react-refresh": "^0.4.19", 24 | "globals": "^15.15.0", 25 | "typescript": "~5.7.2", 26 | "typescript-eslint": "^8.24.1", 27 | "vite": "^6.2.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/examples/dual/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/AUTH.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/ERROR.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/GET.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/USE.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/user/ERROR.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/user/GET.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/user/LOGGER.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/user/POST.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/user/[id]/DELETE.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/user/[id]/PATCH.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/user/[id]/PUT.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/user/[id]/USE.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-isolated/user/confirm/POST.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-legacy/index.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-legacy/user/[id]/index.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-legacy/user/confirm.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/api-legacy/user/index.js: -------------------------------------------------------------------------------- 1 | export default () => {}; 2 | -------------------------------------------------------------------------------- /packages/examples/dual/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | 4 | createRoot(document.getElementById("root")!).render( 5 | 6 |

OK

7 |
8 | ); 9 | -------------------------------------------------------------------------------- /packages/examples/dual/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/examples/dual/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/examples/dual/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/examples/dual/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/examples/dual/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react-swc"; 2 | import { defineConfig } from "vite"; 3 | //import apiRoutes from "vite-plugin-api-routes"; 4 | import apiRoutes from "../../plugin/src"; 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | base: "/myapp", 9 | plugins: [ 10 | react(), 11 | apiRoutes({ 12 | mode: "legacy", 13 | cacheDir: ".api-legacy", 14 | disableBuild: true, 15 | serverMinify: true, 16 | mapper: { 17 | AUTH: { 18 | method: "use", 19 | priority: 11, 20 | }, 21 | ERROR: { 22 | method: "use", 23 | priority: 120, 24 | }, 25 | LOGGER: { 26 | method: "use", 27 | priority: 31, 28 | }, 29 | }, 30 | dirs: [ 31 | { 32 | dir: "src/api-legacy", 33 | route: "", 34 | }, 35 | ], 36 | }), 37 | apiRoutes({ 38 | mode: "isolated", 39 | cacheDir: ".api-isolated", 40 | serverMinify: true, 41 | mapper: { 42 | AUTH: { 43 | method: "use", 44 | priority: 11, 45 | }, 46 | ERROR: { 47 | method: "use", 48 | priority: 120, 49 | }, 50 | LOGGER: { 51 | method: "use", 52 | priority: 31, 53 | }, 54 | }, 55 | dirs: [ 56 | { 57 | dir: "src/api-isolated", 58 | route: "", 59 | }, 60 | ], 61 | }), 62 | ], 63 | }); 64 | -------------------------------------------------------------------------------- /packages/plugin/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/settings.json 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | .api 28 | *.tgz 29 | .yarn 30 | .up-force 31 | .fetch-client -------------------------------------------------------------------------------- /packages/plugin/README.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-api-routes 2 | 3 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 4 | [![Build Status](https://travis-ci.org/yracnet/vite-plugin-api-routes.svg?branch=main)](https://travis-ci.org/yracnet/vite-plugin-api-routes) 5 | 6 | ## Vision 7 | 8 | `vite-plugin-api-routes` enhances API routing in ViteJS based on directory structure for improved visibility and project organization in Node.js and Express applications. 9 | 10 | ## Motivation 11 | 12 | - Simplify project configuration 13 | - Convert the directory tree into automatic routing rules 14 | 15 | ## Routing Modes 16 | 17 | The plugin offers two approaches to define API routes: 18 | 19 | ### LEGACY Mode 20 | 21 | In this approach, **a single file can handle multiple HTTP methods** through named exports. 22 | 23 | **Example structure:** 24 | 25 | ```bash 26 | $ tree src/api-legacy 27 | src/api-legacy 28 | ├── index.js 29 | └── user 30 | ├── [id] 31 | │ └── index.js 32 | ├── confirm.js 33 | └── index.js 34 | ``` 35 | 36 | **Generated mapping:** 37 | 38 | ```bash 39 | USE /api/ src/api-legacy/index.js?fn=default 40 | GET /api/ src/api-legacy/index.js?fn=GET 41 | POST /api/ src/api-legacy/index.js?fn=POST 42 | PATCH /api/ src/api-legacy/index.js?fn=PATCH 43 | PUT /api/ src/api-legacy/index.js?fn=PUT 44 | DELETE /api/ src/api-legacy/index.js?fn=DELETE 45 | USE /api/user/ src/api-legacy/user/index.js?fn=USE 46 | GET /api/user/ src/api-legacy/user/index.js?fn=GET 47 | POST /api/user/ src/api-legacy/user/index.js?fn=POST 48 | PATCH /api/user/ src/api-legacy/user/index.js?fn=PATCH 49 | PUT /api/user/ src/api-legacy/user/index.js?fn=PUT 50 | DELETE /api/user/ src/api-legacy/user/index.js?fn=DELETE 51 | USE /api/user/confirm src/api-legacy/user/confirm.js?fn=default 52 | GET /api/user/confirm src/api-legacy/user/confirm.js?fn=GET 53 | # ... and many more routes 54 | ``` 55 | 56 | **LEGACY mode features:** 57 | 58 | - Simple at the file system level 59 | - One file handles multiple HTTP methods 60 | - Hides the actual route structure 61 | - Ideal for APIs with few endpoints or small projects 62 | 63 | ### ISOLATED Mode 64 | 65 | In this approach, **each HTTP method is defined in a separate file**, which improves the visibility of available routes. 66 | 67 | **Example structure:** 68 | 69 | ```bash 70 | $ tree src/api-isolated 71 | src/api-isolated 72 | ├── GET.js 73 | ├── USE.js 74 | └── user 75 | ├── [id] 76 | │ ├── DELETE.js 77 | │ ├── PATCH.js 78 | │ ├── PUT.js 79 | │ └── USE.js 80 | ├── confirm 81 | │ └── POST.js 82 | ├── GET.js 83 | └── POST.js 84 | ``` 85 | 86 | **Generated mapping:** 87 | 88 | ```log 89 | USE /api/ src/api-isolated/USE.js 90 | GET /api/ src/api-isolated/GET.js 91 | GET /api/user/ src/api-isolated/user/GET.js 92 | POST /api/user/ src/api-isolated/user/POST.js 93 | POST /api/user/confirm/ src/api-isolated/user/confirm/POST.js 94 | USE /api/user/:id/ src/api-isolated/user/[id]/USE.js 95 | PATCH /api/user/:id/ src/api-isolated/user/[id]/PATCH.js 96 | PUT /api/user/:id/ src/api-isolated/user/[id]/PUT.js 97 | DELETE /api/user/:id/ src/api-isolated/user/[id]/DELETE.js 98 | ``` 99 | 100 | **ISOLATED mode features:** 101 | 102 | - More explicit and understandable mapping 103 | - One file per HTTP method 104 | - Clear visibility of available endpoints 105 | - Better organization for complex APIs 106 | - Easier long-term maintenance 107 | 108 | ## Priority Mapping System 109 | 110 | All methods are mapped to routes with priorities defined by the `mapper` attribute, allowing precise control over middleware execution order. 111 | 112 | **Example configuration:** 113 | 114 | ```java 115 | import { defineConfig } from "vite"; 116 | import api from "vite-plugin-api-routes"; 117 | 118 | export default defineConfig({ 119 | plugins: [ 120 | api({ 121 | mapper: { 122 | // Default Mapping 123 | default: { method: "use", priority: 10 }, 124 | USE: { method: "use", priority: 20 }, 125 | GET: { method: "get", priority: 30 }, 126 | POST: { method: "post", priority: 40 }, 127 | PATCH: { method: "patch", priority: 50 }, 128 | PUT: { method: "put", priority: 60 }, 129 | DELETE: { method: "delete", priority: 70 }, 130 | // Custom aliases 131 | AUTH: { method: "use", priority: 11 }, // After default and before USE 132 | ERROR: { method: "use", priority: 120 }, // After DELETE, FILES, and PARAMS 133 | LOGGER: { method: "use", priority: 31 }, // After GET and before POST 134 | }, 135 | filePriority: 100, // Default value for files 136 | paramPriority: 110, // Default value for parameters 137 | }), 138 | ], 139 | }); 140 | ``` 141 | 142 | **Example structure:** 143 | 144 | ```bash 145 | $ tree src/api-isolated/ 146 | src/api-isolated/ 147 | ├── AUTH.js 148 | ├── ERROR.js 149 | ├── GET.js 150 | ├── USE.js 151 | └── user 152 | ├── [id] 153 | │ ├── DELETE.js 154 | │ ├── PATCH.js 155 | │ ├── PUT.js 156 | │ └── USE.js 157 | ├── confirm 158 | │ └── POST.js 159 | ├── ERROR.js 160 | ├── GET.js 161 | ├── LOGGER.js 162 | └── POST.js 163 | ``` 164 | 165 | **Generated mapping (respecting priorities):** 166 | 167 | ```js 168 | USE /api/ src/api-isolated/AUTH.js // after default and before USE 169 | USE /api/ src/api-isolated/USE.js 170 | GET /api/ src/api-isolated/GET.js 171 | GET /api/user/ src/api-isolated/user/GET.js 172 | USE /api/user/ src/api-isolated/user/LOGGER.js // after GET and before POST 173 | POST /api/user/ src/api-isolated/user/POST.js 174 | POST /api/user/confirm/ src/api-isolated/user/confirm/POST.js 175 | USE /api/user/:id/ src/api-isolated/user/[id]/USE.js 176 | PATCH /api/user/:id/ src/api-isolated/user/[id]/PATCH.js 177 | PUT /api/user/:id/ src/api-isolated/user/[id]/PUT.js 178 | DELETE /api/user/:id/ src/api-isolated/user/[id]/DELETE.js 179 | USE /api/user/ src/api-isolated/user/ERROR.js // after DELETE, FILES, and PARAMS 180 | USE /api/ src/api-isolated/ERROR.js // after DELETE, FILES, and PARAMS 181 | ``` 182 | 183 | ## Installation and Basic Configuration 184 | 185 | ### Installation 186 | 187 | ```bash 188 | yarn add vite-plugin-api-routes 189 | ``` 190 | 191 | ### Vite Configuration 192 | 193 | ```js 194 | import { defineConfig } from "vite"; 195 | import api from "vite-plugin-api-routes"; 196 | 197 | export default defineConfig({ 198 | plugins: [ 199 | api(), // with default configuration 200 | ], 201 | }); 202 | ``` 203 | 204 | ### TypeScript Configuration 205 | 206 | To access type definitions, include the generated file in your project: 207 | 208 | In `src/vite-env.d.ts`, add: 209 | 210 | ```ts 211 | /// 212 | ``` 213 | 214 | ## Customization 215 | 216 | The plugin allows customizing three main components: 217 | 218 | - **Handler File**: `/src/handler.js` - Customize the route handler 219 | - **Server File**: `/src/server.ts` - Customize the Express server 220 | - **Configure File**: `/src/configure.ts` - Customize server configuration 221 | 222 | ## Execution Environments 223 | 224 | ### Development Mode 225 | 226 | In development, the plugin serves API routes through the Vite server with HMR support: 227 | 228 | ```bash 229 | yarn dev 230 | ``` 231 | 232 | ### Production Mode 233 | 234 | For production, build your application normally: 235 | 236 | ```bash 237 | yarn build 238 | ``` 239 | 240 | ## Additional Resources 241 | 242 | - **npm Package**: [vite-plugin-api-routes](https://www.npmjs.com/package/vite-plugin-api-routes) 243 | - **GitHub Repository**: [yracnet/vite-plugin-api-routes](https://github.com/yracnet/vite-plugin-api-routes) 244 | - **Articles**: 245 | - [Enhancing API Routing in Vite.js with vite-plugin-api](https://dev.to/yracnet/enhancing-api-routing-in-vitejs-with-vite-plugin-api-p39) 246 | - [CRUD User API + GUI in ViteJS](https://dev.to/yracnet/crud-user-api-gui-in-vitejs-df8) 247 | - **Tutorials**: 248 | - [Tutorial Legacy](./tutorial-legacy.md) 249 | - [Tutorial Isolated](./tutorial-isolated.md) 250 | - [Tutorial CRUD](./tutorial-crud.md) 251 | - **Previous Documentation**: 252 | - [README 1.0](./README_1.0.md) 253 | 254 | ## License 255 | 256 | This project is licensed under the [MIT License](LICENSE). 257 | 258 | ## Version Notes 259 | 260 | ### Project Renaming 261 | 262 | 🙏 **Dear Community,** 263 | 264 | We sincerely apologize for the recent project name changes. After careful consideration and feedback, we've settled on the name **vite-plugin-api-routes**. We understand that these changes might have caused confusion, and we appreciate your understanding. 265 | 266 | Thank you for your continued support and flexibility. 267 | 268 | Best regards, 269 | 270 | [Willyams Yujra](https://github.com/yracnet) 271 | -------------------------------------------------------------------------------- /packages/plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-api-routes", 3 | "version": "1.2.5-beta", 4 | "type": "module", 5 | "description": "A Vite.js plugin that creates API routes by mapping the directory structure, similar to Next.js API Routes. This plugin enhances the functionality for backend development using Vite.", 6 | "keywords": [ 7 | "vite", 8 | "api", 9 | "api-router", 10 | "api-routers", 11 | "vite-plugin-api", 12 | "vite-plugin-rest-api", 13 | "express", 14 | "express-router", 15 | "server", 16 | "file-system-based", 17 | "routing", 18 | "rest-api", 19 | "api-rest", 20 | "plugin", 21 | "API", 22 | "routes", 23 | "backend", 24 | "Remix", 25 | "Next.js" 26 | ], 27 | "files": [ 28 | "dist", 29 | ".api", 30 | "client.d.ts", 31 | "LICENSE" 32 | ], 33 | "license": "MIT", 34 | "author": "Willyams Yujra ", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/yracnet/vite-plugin-api-routes" 38 | }, 39 | "exports": { 40 | ".": { 41 | "types": "./dist/index.d.ts", 42 | "import": "./dist/index.js" 43 | }, 44 | "./configure": { 45 | "types": "./.api/configure.d.ts" 46 | }, 47 | "./handler": { 48 | "types": "./.api/handler.d.ts" 49 | }, 50 | "./routers": { 51 | "types": "./.api/routers.d.ts" 52 | } 53 | }, 54 | "homepage": "https://github.com/yracnet/vite-plugin-api-routes", 55 | "bugs": "https://github.com/yracnet/vite-plugin-api-routes/issues", 56 | "scripts": { 57 | "build": "tsup", 58 | "prepublish": "yarn build", 59 | "prepack": "yarn build" 60 | }, 61 | "dependencies": { 62 | "dotenv-local": "^1.0.2", 63 | "fast-glob": "^3.3.2", 64 | "fs-extra": "^11.2.0", 65 | "slash-path": "^0.0.2", 66 | "vite-plugin-builder": "^1.0.0" 67 | }, 68 | "peerDependencies": { 69 | "vite": "^3 || ^4 || ^5 || ^6 || ^7" 70 | }, 71 | "devDependencies": { 72 | "@types/express": "^5.0.0", 73 | "@types/fs-extra": "^11.0.4", 74 | "@types/node": "^22.10.2", 75 | "express": "^4.21.2", 76 | "tsup": "^8.3.5", 77 | "typescript": "^5.7.2", 78 | "vite": "^6.2.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/plugin/src/api-server/configure.ts: -------------------------------------------------------------------------------- 1 | import express, { ErrorRequestHandler, Express, Handler } from "express"; 2 | import { Callback, RouteInfo } from "vite-plugin-api-routes/handler"; 3 | 4 | export type ViteServerHook = (server: Express, viteServer: any) => void; 5 | 6 | export type ServerHook = (server: Express) => void; 7 | 8 | export type HandlerHook = (handler: Handler) => void; 9 | 10 | export type CallbackHook = (callback: Callback, route: RouteInfo) => Callback; 11 | 12 | export type StatusHook = (server: Express, status: any) => void; 13 | 14 | const errorHandler: ErrorRequestHandler = (error, _, res, next) => { 15 | if (error instanceof Error) { 16 | res.status(403).json({ error: error.message }); 17 | } else { 18 | next(error); 19 | } 20 | }; 21 | 22 | export const viteServerBefore: ViteServerHook = (server) => { 23 | server.use(express.json()); 24 | server.use(express.urlencoded({ extended: true })); 25 | }; 26 | 27 | export const viteServerAfter: ViteServerHook = (server) => { 28 | server.use(errorHandler); 29 | }; 30 | 31 | export const serverBefore: ServerHook = (server) => { 32 | server.use(express.json()); 33 | server.use(express.urlencoded({ extended: true })); 34 | }; 35 | 36 | export const serverAfter: ServerHook = (server) => { 37 | server.use(errorHandler); 38 | }; 39 | 40 | export const handlerBefore: HandlerHook = () => {}; 41 | 42 | export const handlerAfter: HandlerHook = () => {}; 43 | 44 | export const callbackBefore: CallbackHook = (callback) => { 45 | return callback; 46 | }; 47 | 48 | export const serverListening: StatusHook = () => { 49 | console.log(`Server Running`); 50 | }; 51 | 52 | export const serverError: StatusHook = (_, error) => { 53 | console.log(`Server Error: `, error); 54 | }; 55 | -------------------------------------------------------------------------------- /packages/plugin/src/api-server/env.ts: -------------------------------------------------------------------------------- 1 | declare module "@api/configure" { 2 | export * from "vite-plugin-api-routes/configure"; 3 | } 4 | declare module "@api/handler" { 5 | export * from "vite-plugin-api-routes/handler"; 6 | } 7 | declare module "@api/routers" { 8 | export * from "vite-plugin-api-routes/routers"; 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin/src/api-server/handler.ts: -------------------------------------------------------------------------------- 1 | import express, { ErrorRequestHandler, Express, RequestHandler } from "express"; 2 | import * as configure from "vite-plugin-api-routes/configure"; 3 | import { applyRouters } from "vite-plugin-api-routes/routers"; 4 | 5 | export type Callback = 6 | | ErrorRequestHandler 7 | | RequestHandler 8 | | (ErrorRequestHandler | RequestHandler)[]; 9 | 10 | export type RouteInfo = { 11 | source: string; 12 | method: "get" | "post" | "put" | "push" | "delete" | string; 13 | route: string; 14 | path: string; 15 | url: string; 16 | }; 17 | 18 | export type RouteModule = RouteInfo & { 19 | cb: Callback; 20 | }; 21 | 22 | export const handler: Express = express(); 23 | 24 | configure.handlerBefore?.(handler); 25 | 26 | applyRouters((props: RouteModule) => { 27 | const { method, route, path, cb } = props; 28 | //@ts-ignore 29 | if (handler[method]) { 30 | if (Array.isArray(cb)) { 31 | //@ts-ignore 32 | handler[method](route, ...cb); 33 | } else { 34 | //@ts-ignore 35 | handler[method](route, cb); 36 | } 37 | } else { 38 | console.log("Not Support", method, "for", route, "in", handler); 39 | } 40 | }); 41 | 42 | configure.handlerAfter?.(handler); 43 | -------------------------------------------------------------------------------- /packages/plugin/src/api-server/routers.ts: -------------------------------------------------------------------------------- 1 | import * as configure from "vite-plugin-api-routes/configure"; 2 | import { RouteInfo, RouteModule } from "vite-plugin-api-routes/handler"; 3 | 4 | export type ApplyRouter = (route: RouteModule) => void; 5 | 6 | export type ApplyRouters = (apply: ApplyRouter) => void; 7 | 8 | export const routeBase: string = "/api"; 9 | 10 | const internal: RouteModule[] = []; 11 | 12 | export const routers: RouteInfo[] = []; 13 | 14 | export const endpoints: string[] = []; 15 | 16 | export const applyRouters: ApplyRouters = (applyRouter) => { 17 | internal.forEach((it) => { 18 | it.cb = configure.callbackBefore?.(it.cb, it) || it.cb; 19 | applyRouter(it); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/plugin/src/api-server/server.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from "dotenv-local"; 2 | import express from "express"; 3 | import * as configure from "vite-plugin-api-routes/configure"; 4 | import { handler } from "vite-plugin-api-routes/handler"; 5 | import { endpoints } from "vite-plugin-api-routes/routers"; 6 | 7 | const server = express(); 8 | configure.serverBefore?.(server); 9 | 10 | const { HOST, PORT } = loadEnv({ 11 | envPrefix: "SERVER_", 12 | removeEnvPrefix: true, 13 | envInitial: { 14 | SERVER_HOST: "127.0.0.1", 15 | SERVER_PORT: "3000", 16 | }, 17 | }); 18 | 19 | const SERVER_URL = `http://${HOST}:${PORT}${API_ROUTES.BASE}`; 20 | 21 | server.use(API_ROUTES.BASE_API, handler); 22 | server.use(API_ROUTES.BASE, express.static(API_ROUTES.PUBLIC_DIR)); 23 | 24 | configure.serverAfter?.(server); 25 | 26 | const PORT_NRO = parseInt(PORT); 27 | server 28 | .listen(PORT_NRO, HOST, () => { 29 | console.log(`Ready at ${SERVER_URL}`); 30 | configure.serverListening?.(server, endpoints); 31 | }) 32 | .on("error", (error) => { 33 | console.error(`Error at ${SERVER_URL}`, error); 34 | configure.serverError?.(server, error); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/plugin/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare var API_ROUTES: any; 4 | -------------------------------------------------------------------------------- /packages/plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginOption } from "vite"; 2 | import { ApiOpts, assertConfig } from "./model"; 3 | import { apiRoutesBuild } from "./plugin-build"; 4 | import { apiRoutesRoute } from "./plugin-route"; 5 | import { apiRoutesServe } from "./plugin-serve"; 6 | import { cleanDirectory, copyFilesDirectory, findDirPlugin } from "./utils"; 7 | 8 | export const pluginAPIRoutes = (opts: ApiOpts = {}): PluginOption => { 9 | const apiConfig = assertConfig(opts); 10 | const apiDir = findDirPlugin(".api"); 11 | cleanDirectory(apiConfig.cacheDir); 12 | copyFilesDirectory(apiDir, apiConfig.cacheDir, { 13 | files: ["configure.js", "handler.js", "server.js"], 14 | oldId: "vite-plugin-api-routes", 15 | newId: apiConfig.moduleId, 16 | }); 17 | copyFilesDirectory(apiDir, apiConfig.cacheDir, { 18 | files: ["env.d.ts"], 19 | oldId: "@api", 20 | newId: apiConfig.moduleId, 21 | }); 22 | return [ 23 | // 24 | apiRoutesRoute(apiConfig), 25 | apiRoutesServe(apiConfig), 26 | apiRoutesBuild(apiConfig), 27 | ]; 28 | }; 29 | 30 | export const pluginAPI = pluginAPIRoutes; 31 | 32 | export const createAPI = pluginAPIRoutes; 33 | 34 | export default pluginAPIRoutes; 35 | -------------------------------------------------------------------------------- /packages/plugin/src/model.ts: -------------------------------------------------------------------------------- 1 | import path from "slash-path"; 2 | import { InlineConfig } from "vite"; 3 | 4 | export type DirRoute = { 5 | dir: string; 6 | route: string; 7 | exclude?: string[]; 8 | skip?: "development" | "production" | string | boolean; 9 | }; 10 | 11 | export type Mapper = { 12 | order: number; 13 | name: string; 14 | method: string; 15 | }; 16 | 17 | type MethodConfig = { name: string; method: string; priority: string }; 18 | 19 | export type ApiConfig = { 20 | moduleId: string; 21 | mode: "legacy" | "isolated"; 22 | server: string; 23 | handler: string; 24 | configure: string; 25 | serverFile: string; 26 | handlerFile: string; 27 | routersFile: string; 28 | configureFile: string; 29 | dirs: DirRoute[]; 30 | include: string[]; 31 | exclude: string[]; 32 | mapperList: MethodConfig[]; 33 | filePriority: string; 34 | paramPriority: string; 35 | watcherList: string[]; 36 | routeBase: string; 37 | root: string; 38 | cacheDir: string; 39 | forceRestart: boolean; 40 | disableBuild: boolean; 41 | clientOutDir: string; 42 | clientMinify: boolean | "terser" | "esbuild"; 43 | clientBuild: (config: InlineConfig) => InlineConfig; 44 | serverOutDir: string; 45 | serverMinify: boolean | "terser" | "esbuild"; 46 | serverBuild: (config: InlineConfig) => InlineConfig; 47 | }; 48 | 49 | export type ApiOpts = { 50 | moduleId?: string; 51 | mode?: "legacy" | "isolated"; 52 | server?: string; 53 | handler?: string; 54 | configure?: string; 55 | dirs?: DirRoute[]; 56 | include?: string[]; 57 | exclude?: string[]; 58 | mapper?: { 59 | [k: string]: false | string | { method: string; priority: number }; 60 | }; 61 | filePriority?: number; 62 | paramPriority?: number; 63 | routeBase?: string; 64 | root?: string; 65 | cacheDir?: string; 66 | forceRestart?: boolean; 67 | disableBuild?: boolean; 68 | clientOutDir?: string; 69 | clientMinify?: boolean | "terser" | "esbuild"; 70 | clientBuild?: (config: InlineConfig) => InlineConfig; 71 | serverOutDir?: string; 72 | serverMinify?: boolean | "terser" | "esbuild"; 73 | serverBuild?: (config: InlineConfig) => InlineConfig; 74 | }; 75 | 76 | export const assertConfig = (opts: ApiOpts): ApiConfig => { 77 | let { 78 | moduleId = "@api", 79 | mode = "legacy", 80 | root = process.cwd(), 81 | cacheDir = ".api", 82 | server = path.join(cacheDir, "server.js"), 83 | handler = path.join(cacheDir, "handler.js"), 84 | configure = path.join(cacheDir, "configure.js"), 85 | routeBase = "api", 86 | dirs = [{ dir: "src/api", route: "", exclude: [], skip: false }], 87 | include = ["**/*.ts", "**/*.js"], 88 | exclude = [], 89 | mapper = {}, 90 | filePriority = 100, 91 | paramPriority = 110, 92 | forceRestart = false, 93 | disableBuild = false, 94 | clientOutDir = "dist/client", 95 | clientMinify = false, 96 | clientBuild = (config: InlineConfig) => config, 97 | serverOutDir = "dist", 98 | serverMinify = false, 99 | serverBuild = (config: InlineConfig) => config, 100 | } = opts; 101 | if (moduleId !== "@api") { 102 | console.warn("The moduleId will be remove in the next release"); 103 | } 104 | if (cacheDir !== ".api") { 105 | console.warn("The cacheDir will be remove in the next release"); 106 | } 107 | cacheDir = path.join(root, cacheDir); 108 | 109 | dirs = dirs.map((it) => { 110 | it.dir = path.join(root, it.dir); 111 | return it; 112 | }); 113 | 114 | mapper = { 115 | default: { method: "use", priority: 10 }, 116 | USE: { method: "use", priority: 20 }, 117 | GET: { method: "get", priority: 30 }, 118 | POST: { method: "post", priority: 40 }, 119 | PATCH: { method: "patch", priority: 50 }, 120 | PUT: { method: "put", priority: 60 }, 121 | DELETE: { method: "delete", priority: 70 }, 122 | // Overwrite 123 | ...mapper, 124 | }; 125 | if (mode === "isolated") { 126 | delete mapper.default; 127 | } 128 | 129 | routeBase = path.join("/", routeBase); 130 | clientOutDir = path.join(root, clientOutDir); 131 | serverOutDir = path.join(root, serverOutDir); 132 | const serverFile = path.join(root, server); 133 | const handlerFile = path.join(root, handler); 134 | const routersFile = path.join(cacheDir, "routers.js"); 135 | const configureFile = path.join(root, configure); 136 | const mapperList: MethodConfig[] = Object.entries(mapper) 137 | .map(([name, value]) => { 138 | if (value === false) { 139 | return { 140 | name, 141 | method: "", 142 | priority: "000", 143 | }; 144 | } 145 | if (typeof value === "string") { 146 | return { 147 | name, 148 | method: value, 149 | priority: "010", 150 | }; 151 | } 152 | return { 153 | name, 154 | method: value.method, 155 | priority: value.priority.toString().padStart(3, "0"), 156 | }; 157 | }) 158 | .filter((it) => it.method !== ""); 159 | if (mode === "isolated") { 160 | include = mapperList.map((it) => `**/${it.name}.{js,ts}`); 161 | } 162 | const watcherList = dirs.map((it) => it.dir); 163 | //watcherList.push(routersFile); 164 | watcherList.push(configureFile); 165 | watcherList.push(handlerFile); 166 | 167 | return { 168 | mode, 169 | moduleId, 170 | server, 171 | handler, 172 | configure, 173 | root, 174 | serverFile, 175 | handlerFile, 176 | routersFile, 177 | filePriority: filePriority.toString().padStart(3, "0"), 178 | paramPriority: paramPriority.toString().padStart(3, "0"), 179 | configureFile, 180 | routeBase, 181 | dirs, 182 | include, 183 | exclude, 184 | mapperList, 185 | watcherList, 186 | cacheDir, 187 | forceRestart, 188 | disableBuild, 189 | clientOutDir, 190 | clientMinify, 191 | clientBuild, 192 | serverOutDir, 193 | serverMinify, 194 | serverBuild, 195 | }; 196 | }; 197 | -------------------------------------------------------------------------------- /packages/plugin/src/plugin-build/index.ts: -------------------------------------------------------------------------------- 1 | import path from "slash-path"; 2 | import { PluginOption, ResolvedConfig, build } from "vite"; 3 | import { ApiConfig } from "../model"; 4 | const ENTRY_NONE = "____.html"; 5 | 6 | const simplePath = (filePath: string) => 7 | filePath 8 | .replace(/(\.[^\.]+)$/, "") 9 | .replace(/^[\/\\]/gi, "") 10 | .replace(/[\/\\]$/, ""); 11 | 12 | const doBuildServer = async ( 13 | apiConfig: ApiConfig, 14 | viteConfig: ResolvedConfig 15 | ) => { 16 | const { 17 | root, 18 | serverOutDir, 19 | clientOutDir, 20 | serverMinify, 21 | serverBuild, 22 | routeBase, 23 | serverFile, 24 | configureFile, 25 | handlerFile, 26 | routersFile, 27 | cacheDir, 28 | } = apiConfig; 29 | const binFiles = [configureFile, handlerFile, routersFile]; 30 | const clientDir = path.relative(serverOutDir, clientOutDir); 31 | const viteServer = await serverBuild({ 32 | appType: "custom", 33 | root, 34 | publicDir: "private", 35 | define: { 36 | "API_ROUTES.BASE": JSON.stringify(viteConfig.base), 37 | "API_ROUTES.BASE_API": JSON.stringify( 38 | path.join(viteConfig.base, routeBase) 39 | ), 40 | "API_ROUTES.PUBLIC_DIR": JSON.stringify(clientDir), 41 | }, 42 | build: { 43 | outDir: serverOutDir, 44 | ssr: serverFile, 45 | write: true, 46 | minify: serverMinify, 47 | target: "esnext", 48 | emptyOutDir: true, 49 | rollupOptions: { 50 | external: viteConfig.build?.rollupOptions?.external, 51 | output: { 52 | format: "es", 53 | entryFileNames: "app.js", 54 | chunkFileNames: "bin/[name]-[hash].js", 55 | assetFileNames: "assets/[name].[ext]", 56 | manualChunks: (id) => { 57 | const isApi = apiConfig.dirs.find((it) => id.includes(it.dir)); 58 | if (isApi) { 59 | const file = path.join( 60 | apiConfig.routeBase || "", 61 | isApi.route || "", 62 | path.relative(isApi.dir, id) 63 | ); 64 | return simplePath(file); 65 | } 66 | const isBin = binFiles.find((bin) => id.includes(bin)); 67 | if (isBin) { 68 | return "index"; 69 | } 70 | if (id.includes("node_modules")) { 71 | const pkg = id.split("node_modules/")[1]; 72 | const lib = path.join("libs", pkg); 73 | return simplePath(lib); 74 | } 75 | }, 76 | }, 77 | onwarn: (warning: any, handler: any) => { 78 | if ( 79 | warning.code === "MISSING_EXPORT" && 80 | warning.id.startsWith(cacheDir) 81 | ) { 82 | return; 83 | } 84 | handler(warning); 85 | }, 86 | }, 87 | }, 88 | }); 89 | await build(viteServer); 90 | }; 91 | const doBuildClient = async ( 92 | apiConfig: ApiConfig, 93 | viteConfig: ResolvedConfig 94 | ) => { 95 | const { root, clientOutDir, clientMinify, clientBuild } = apiConfig; 96 | const preloadFiles = [ 97 | "modulepreload-polyfill", 98 | "commonjsHelpers", 99 | "vite/", 100 | "installHook", 101 | ]; 102 | const viteClient = await clientBuild({ 103 | root, 104 | build: { 105 | outDir: clientOutDir, 106 | write: true, 107 | minify: clientMinify, 108 | emptyOutDir: true, 109 | rollupOptions: { 110 | external: viteConfig.build?.rollupOptions?.external, 111 | output: { 112 | manualChunks: (id) => { 113 | const isInternal = preloadFiles.find((it) => id.includes(it)); 114 | if (isInternal) { 115 | return "preload"; 116 | } 117 | if (id.includes("node_modules")) { 118 | return "vendor"; 119 | } 120 | }, 121 | }, 122 | }, 123 | }, 124 | }); 125 | await build(viteClient); 126 | }; 127 | 128 | export const apiRoutesBuild = (apiConfig: ApiConfig): PluginOption => { 129 | if (apiConfig.disableBuild) { 130 | return null; 131 | } 132 | //@ts-ignore 133 | let viteConfig: ResolvedConfig = {}; 134 | return { 135 | name: "vite-plugin-api-routes:build", 136 | enforce: "pre", 137 | apply: "build", 138 | config: () => { 139 | if (process.env.IS_API_ROUTES_BUILD) return {}; 140 | return { 141 | build: { 142 | emptyOutDir: true, 143 | copyPublicDir: false, 144 | write: false, 145 | rollupOptions: { 146 | input: { 147 | main: ENTRY_NONE, 148 | }, 149 | }, 150 | }, 151 | }; 152 | }, 153 | resolveId: (id) => { 154 | if (id === ENTRY_NONE) { 155 | return id; 156 | } 157 | return null; 158 | }, 159 | load: (id) => { 160 | if (id === ENTRY_NONE) { 161 | return ""; 162 | } 163 | return null; 164 | }, 165 | configResolved: (config) => { 166 | viteConfig = config; 167 | }, 168 | buildStart: async () => { 169 | if (process.env.IS_API_ROUTES_BUILD) return; 170 | process.env.IS_API_ROUTES_BUILD = "true"; 171 | viteConfig.logger.info(""); 172 | viteConfig.logger.info("\x1b[1m\x1b[31mSERVER BUILD\x1b[0m"); 173 | await doBuildServer(apiConfig, viteConfig); 174 | viteConfig.logger.info(""); 175 | viteConfig.logger.info("\x1b[1m\x1b[31mCLIENT BUILD\x1b[0m"); 176 | await doBuildClient(apiConfig, viteConfig); 177 | viteConfig.logger.info(""); 178 | }, 179 | }; 180 | }; 181 | -------------------------------------------------------------------------------- /packages/plugin/src/plugin-route/common.ts: -------------------------------------------------------------------------------- 1 | import fg from "fast-glob"; 2 | import path from "slash-path"; 3 | import { ApiConfig } from "../model"; 4 | 5 | type Key = { key: string }; 6 | const byKey = (a: Key, b: Key) => a.key.localeCompare(b.key); 7 | 8 | const isParam = (name: string) => /:|\[|\$/.test(name); 9 | 10 | export const createKeyRoute = (route: string, apiConfig: ApiConfig) => { 11 | return route 12 | .split("/") 13 | .filter((it) => it) 14 | .map((n) => { 15 | //const s = "_" + n.padEnd(6, "_"); 16 | const s = "_" + n; 17 | const p = apiConfig.mapperList.find((r) => r.name === n); 18 | if (p) { 19 | return p.priority + s; 20 | } else if (isParam(n)) { 21 | return apiConfig.paramPriority + s; 22 | } 23 | return apiConfig.filePriority + s; 24 | }) 25 | .join("_"); 26 | }; 27 | 28 | export const parseFileToRoute = (names: string) => { 29 | const route = names 30 | .split("/") 31 | .map((name) => { 32 | name = name 33 | // Param Remix 34 | .replaceAll("$", ":") 35 | // Param NextJS 36 | .replaceAll("[", ":") 37 | .replaceAll("]", ""); 38 | return name; 39 | }) 40 | .join("/"); 41 | return ( 42 | route 43 | // Remove Extension 44 | .replace(/\.[^.]+$/, "") 45 | // Remove Index 46 | .replaceAll(/index$/gi, "") 47 | // Remove Index 48 | .replaceAll(/_index$/gi, "") 49 | ); 50 | }; 51 | 52 | export type FileRouter = { 53 | varName: string; 54 | name: string; 55 | file: string; 56 | route: string; 57 | key: string; 58 | }; 59 | 60 | export const getAllFileRouters = (apiConfig: ApiConfig): FileRouter[] => { 61 | let { dirs, include, exclude } = apiConfig; 62 | const currentMode = process.env.NODE_ENV; 63 | return dirs 64 | .filter((dir) => { 65 | if (dir.skip === true || dir.skip === currentMode) { 66 | return false; 67 | } 68 | return true; 69 | }) 70 | .flatMap((it) => { 71 | it.exclude = it.exclude || []; 72 | const ignore = [...exclude, ...it.exclude]; 73 | const files: string[] = fg.sync(include, { 74 | ignore, 75 | onlyDirectories: false, 76 | dot: true, 77 | unique: true, 78 | cwd: it.dir, 79 | }); 80 | return files.map((file) => { 81 | const routeFile = path.join("/", it.route, file); 82 | const route = parseFileToRoute(routeFile); 83 | const key = createKeyRoute(route, apiConfig); 84 | const relativeFile = path.relative( 85 | apiConfig.root, 86 | path.join(it.dir, file) 87 | ); 88 | return { 89 | name: path.basename(routeFile), 90 | file: relativeFile, 91 | route, 92 | key, 93 | }; 94 | }); 95 | }) 96 | .map((it) => ({ 97 | varName: "API_", 98 | name: it.name, 99 | file: it.file, 100 | route: it.route, 101 | key: it.key, 102 | })) 103 | .sort(byKey) 104 | .map((it, ix) => { 105 | it.varName = "API_" + ix.toString().padStart(3, "0"); 106 | return it; 107 | }); 108 | }; 109 | 110 | export type MethodRouter = { 111 | key: string; 112 | source: string; 113 | method: string; 114 | route: string; 115 | url: string; 116 | cb: string; 117 | }; 118 | 119 | export const parseMethodRouters = ( 120 | fileRoutes: FileRouter[], 121 | apiConfig: ApiConfig 122 | ): MethodRouter[] => { 123 | if (apiConfig.mode === "isolated") { 124 | return fileRoutes 125 | .map((r) => { 126 | const m = apiConfig.mapperList.find((m) => 127 | r.route.endsWith(`/${m.name}`) 128 | ); 129 | if (m == null) { 130 | return null; 131 | } 132 | const re = new RegExp(`${m.name}$`); 133 | const route = r.route.replace(re, ""); 134 | return { 135 | key: r.key, 136 | source: r.file, 137 | method: m.method, 138 | route, 139 | url: path.join("/", apiConfig.routeBase, route), 140 | cb: r.varName + ".default", 141 | }; 142 | }) 143 | .filter((r) => !!r); 144 | } 145 | 146 | return fileRoutes 147 | .flatMap((it) => { 148 | return apiConfig.mapperList.map((m) => { 149 | const route = path.join(it.route, m.name); 150 | const key = createKeyRoute(route, apiConfig); 151 | return { 152 | ...it, 153 | key, 154 | name: m.name, 155 | method: m.method, 156 | }; 157 | }); 158 | }) 159 | .sort(byKey) 160 | .map((it) => { 161 | let cb = it.varName + "." + it.name; 162 | const source = it.file + "?fn=" + it.name; 163 | const route = it.route; 164 | return { 165 | key: it.key, 166 | source, 167 | method: it.method, 168 | route, 169 | url: path.join("/", apiConfig.routeBase, route), 170 | cb, 171 | }; 172 | }); 173 | }; 174 | -------------------------------------------------------------------------------- /packages/plugin/src/plugin-route/index.ts: -------------------------------------------------------------------------------- 1 | import path from "slash-path"; 2 | import { PluginOption } from "vite"; 3 | import { ApiConfig } from "../model"; 4 | import { writeRoutersFile } from "./routersFile"; 5 | 6 | export const apiRoutesRoute = (apiConfig: ApiConfig): PluginOption => { 7 | const isReload = (file: string) => { 8 | file = path.slash(file); 9 | return apiConfig.watcherList.find((it) => file.startsWith(it)); 10 | }; 11 | return { 12 | name: "vite-plugin-api-routes:route", 13 | enforce: "pre", 14 | config: () => { 15 | return { 16 | resolve: { 17 | alias: { 18 | [`${apiConfig.moduleId}/root`]: apiConfig.root, 19 | [`${apiConfig.moduleId}/server`]: apiConfig.serverFile, 20 | [`${apiConfig.moduleId}/handler`]: apiConfig.handlerFile, 21 | [`${apiConfig.moduleId}/routers`]: apiConfig.routersFile, 22 | [`${apiConfig.moduleId}/configure`]: apiConfig.configureFile, 23 | }, 24 | }, 25 | }; 26 | }, 27 | configResolved: (viteConfig) => { 28 | writeRoutersFile(apiConfig, viteConfig); 29 | }, 30 | handleHotUpdate: async (data) => { 31 | if (isReload(data.file)) { 32 | return []; 33 | } 34 | }, 35 | configureServer: async (devServer) => { 36 | const { 37 | // 38 | watcher, 39 | restart, 40 | config: viteConfig, 41 | } = devServer; 42 | const onReload = (file: string) => { 43 | if (isReload(file)) { 44 | writeRoutersFile(apiConfig, viteConfig); 45 | watcher.off("add", onReload); 46 | watcher.off("change", onReload); 47 | if (apiConfig.forceRestart) { 48 | restart(true); 49 | } 50 | } 51 | }; 52 | watcher.on("add", onReload); 53 | watcher.on("change", onReload); 54 | }, 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /packages/plugin/src/plugin-route/routersFile.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "slash-path"; 3 | import { ResolvedConfig } from "vite"; 4 | import { ApiConfig } from "../model"; 5 | import { getAllFileRouters, MethodRouter, parseMethodRouters } from "./common"; 6 | 7 | export const writeRoutersFile = ( 8 | apiConfig: ApiConfig, 9 | vite: ResolvedConfig 10 | ) => { 11 | const { moduleId, cacheDir, routersFile } = apiConfig; 12 | if (routersFile.startsWith(cacheDir)) { 13 | const fileRouters = getAllFileRouters(apiConfig); 14 | const methodRouters = parseMethodRouters(fileRouters, apiConfig); 15 | const max = methodRouters 16 | .map((it) => { 17 | it.url = path.join("/", vite.base, apiConfig.routeBase, it.route); 18 | return it; 19 | }) 20 | .reduce( 21 | (max: any, it) => { 22 | const cb = it.cb.length; 23 | const url = it.url.length; 24 | const route = it.route.length; 25 | const method = it.method.length; 26 | const source = it.source.length; 27 | max.cb = cb > max.cb ? cb : max.cb; 28 | max.url = url > max.url ? url : max.url; 29 | max.route = route > max.route ? route : max.route; 30 | max.method = method > max.method ? method : max.method; 31 | max.source = source > max.source ? source : max.source; 32 | return max; 33 | }, 34 | { cb: 0, method: 0, route: 0, url: 0, source: 0 } 35 | ); 36 | const debug = methodRouters 37 | .map( 38 | (it) => 39 | "// " + 40 | it.method.toUpperCase().padEnd(max.method + 1, " ") + 41 | it.url.padEnd(max.url + 4, " ") + 42 | it.source 43 | ) 44 | .join("\n") 45 | .trim(); 46 | 47 | const writeRouter = (c: MethodRouter) => { 48 | return [ 49 | " ", 50 | `${c.cb}`.padEnd(max.cb + 1, " "), 51 | ` && { cb: `, 52 | `${c.cb}`.padEnd(max.cb + 1, " "), 53 | ", method: ", 54 | `"${c.method}"`.padEnd(max.method + 3, " "), 55 | `, route: `, 56 | `"${c.route}"`.padEnd(max.route + 3, " "), 57 | ", url: ", 58 | `"${c.url}"`.padEnd(max.url + 3, " "), 59 | ", source: ", 60 | `"${c.source}"`.padEnd(max.source + 3, " "), 61 | "}", 62 | ].join(""); 63 | }; 64 | 65 | const importFiles = fileRouters 66 | .map( 67 | (it) => `import * as ${it.varName} from "${moduleId}/root/${it.file}";` 68 | ) 69 | .join("\n"); 70 | const internalRouter = methodRouters.map((c) => writeRouter(c)).join(",\n"); 71 | const code = ` 72 | // Files Imports 73 | import * as configure from "${moduleId}/configure"; 74 | ${importFiles} 75 | 76 | // Public RESTful API Methods and Paths 77 | // This section describes the available HTTP methods and their corresponding endpoints (paths). 78 | ${debug} 79 | 80 | const internal = [ 81 | ${internalRouter} 82 | ].filter(it => it); 83 | 84 | export const routers = internal.map((it) => { 85 | const { method, route, url, source } = it; 86 | return { method, url, route, source }; 87 | }); 88 | 89 | export const endpoints = internal.map( 90 | (it) => it.method?.toUpperCase() + "\\t" + it.url 91 | ); 92 | 93 | export const applyRouters = (applyRouter) => { 94 | internal.forEach((it) => { 95 | it.cb = configure.callbackBefore?.(it.cb, it) || it.cb; 96 | applyRouter(it); 97 | }); 98 | }; 99 | 100 | `; 101 | fs.writeFileSync(routersFile, code); 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /packages/plugin/src/plugin-serve/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import path from "slash-path"; 3 | import { PluginOption } from "vite"; 4 | import { ApiConfig } from "../model"; 5 | 6 | export const apiRoutesServe = (config: ApiConfig): PluginOption => { 7 | return { 8 | name: "vite-plugin-api-routes:serve", 9 | enforce: "pre", 10 | apply: "serve", 11 | configureServer: async (devServer) => { 12 | const { 13 | // 14 | config: vite, 15 | middlewares, 16 | ssrLoadModule, 17 | ssrFixStacktrace, 18 | } = devServer; 19 | const baseApi = path.join(vite.base, config.routeBase); 20 | const { viteServerBefore, viteServerAfter } = await ssrLoadModule( 21 | config.configure, 22 | { 23 | fixStacktrace: true, 24 | } 25 | ); 26 | 27 | var appServer = express(); 28 | //@ts-ignore 29 | viteServerBefore?.(appServer, devServer, vite); 30 | // Register Proxy After Vite Inicialize 31 | appServer.use("/", async (req, res, next) => { 32 | try { 33 | const { handler } = await ssrLoadModule(config.handler, { 34 | fixStacktrace: true, 35 | }); 36 | handler(req, res, next); 37 | } catch (error) { 38 | ssrFixStacktrace(error as Error); 39 | process.exitCode = 1; 40 | next(error); 41 | } 42 | }); 43 | //@ts-ignore 44 | viteServerAfter?.(appServer, devServer, vite); 45 | middlewares.use(baseApi, appServer); 46 | }, 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /packages/plugin/src/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import path from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | export const getPluginDirectory = () => { 6 | if (typeof __dirname === "undefined") { 7 | const filename = fileURLToPath(import.meta.url); 8 | return path.dirname(filename); 9 | } else { 10 | return __dirname; 11 | } 12 | }; 13 | 14 | export const findDirPlugin = (dirname: string, max = 5) => { 15 | const basedir = getPluginDirectory(); 16 | let relative = "/"; 17 | let dirPath = ""; 18 | for (var i = 0; i < max; i++) { 19 | dirPath = path.join(basedir, relative, dirname); 20 | if (fs.existsSync(dirPath)) { 21 | return dirPath; 22 | } 23 | relative += "../"; 24 | } 25 | throw Error(`Not found: ${dirPath}`); 26 | }; 27 | 28 | export const cleanDirectory = (target: string) => { 29 | if (fs.existsSync(target)) { 30 | fs.rmSync(target, { recursive: true, force: true }); 31 | } 32 | fs.mkdirSync(target, { recursive: true }); 33 | }; 34 | 35 | export const copyFilesDirectory = ( 36 | origin: string, 37 | target: string, 38 | { 39 | files = [], 40 | oldId = "", 41 | newId = "", 42 | }: { 43 | files: string[]; 44 | oldId: string; 45 | newId: string; 46 | } 47 | ) => { 48 | files.forEach((file) => { 49 | const sourceFilePath = path.join(origin, file); 50 | const targetFilePath = path.join(target, file); 51 | if (oldId !== newId) { 52 | let fileContent = fs.readFileSync(sourceFilePath, "utf-8"); 53 | fileContent = fileContent.replace(new RegExp(oldId, "g"), newId); 54 | fs.writeFileSync(targetFilePath, fileContent, "utf-8"); 55 | } else { 56 | fs.copySync(sourceFilePath, targetFilePath, { overwrite: true }); 57 | } 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /packages/plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ES2020", 5 | "lib": [ 6 | "ESNext", 7 | "DOM" 8 | ], 9 | "allowJs": true, 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "noUnusedParameters": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "vite-plugin-api-routes/configure": [ 20 | "src/api-server/configure.ts" 21 | ], 22 | "vite-plugin-api-routes/handler": [ 23 | "src/api-server/handler.ts" 24 | ], 25 | "vite-plugin-api-routes/routers": [ 26 | "src/api-server/routers.ts" 27 | ] 28 | } 29 | }, 30 | "include": [ 31 | "src/**/*", 32 | "src/*.d.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules", 36 | "example", 37 | "dist", 38 | ".api" 39 | ] 40 | } -------------------------------------------------------------------------------- /packages/plugin/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ["src/index.ts"], 6 | format: ["esm"], 7 | outDir: "dist", 8 | external: ["vite", "express", "fast-glob", "slash-path"], 9 | dts: true, 10 | clean: true, 11 | sourcemap: false, 12 | minify: false, 13 | }, 14 | { 15 | entry: [ 16 | "src/api-server/configure.ts", 17 | "src/api-server/handler.ts", 18 | "src/api-server/routers.ts", 19 | "src/api-server/server.ts", 20 | "src/api-server/env.ts", 21 | ], 22 | format: ["esm"], 23 | outDir: ".api", 24 | external: [ 25 | "vite", 26 | "express", 27 | "fast-glob", 28 | "slash-path", 29 | "vite-plugin-api-routes/configure", 30 | "vite-plugin-api-routes/handler", 31 | "vite-plugin-api-routes/routers", 32 | "vite-plugin-api-routes/server", 33 | ], 34 | dts: true, 35 | clean: true, 36 | sourcemap: false, 37 | minify: false, 38 | target: "esnext", 39 | }, 40 | ]); 41 | -------------------------------------------------------------------------------- /packages/plugin/tutorial-isolated.md: -------------------------------------------------------------------------------- 1 | ## Tutorial: Isolated API Routing in ViteJS with `vite-plugin-api-routes` 2 | 3 | ### Introduction 4 | 5 | In this tutorial, we will learn how to enhance isolated API routing in ViteJS using the `vite-plugin-api-routes` plugin. This plugin improves visibility and project structure in Node.js and Express by mapping the directory structure to route rules. 6 | 7 | ### Prerequisites 8 | 9 | Before we begin, make sure you have the following: 10 | 11 | - Basic knowledge of ViteJS, Node.js, and Express. 12 | - An existing ViteJS project. 13 | - Read the preview tutorial [Enhancing API Routing in Vite.js with vite-plugin-api](https://dev.to/yracnet/enhancing-api-routing-in-vitejs-with-vite-plugin-api-p39) 14 | 15 | ### Step 1: Installation 16 | 17 | To install the `vite-plugin-api-routes` plugin, run the following command: 18 | 19 | ```bash 20 | yarn add vite-plugin-api-routes 21 | ``` 22 | 23 | ### Step 2: Configuration 24 | 25 | In your ViteJS project, open the `vite.config.ts` file and add the following code: 26 | 27 | ```js 28 | import { defineConfig } from "vite"; 29 | import apiRoutes from "vite-plugin-api-routes"; 30 | 31 | export default defineConfig({ 32 | plugins: [ 33 | apiRoutes({ 34 | mode: "isolated", 35 | // Configuration options go here 36 | }), 37 | ], 38 | }); 39 | ``` 40 | 41 | ### Step 3: Directory Structure 42 | 43 | Next, let's create the API directory structure in your project's source folder (`src/api`). Here's an example structure: 44 | 45 | ```bash 46 | > tree src/api/ 47 | src/api/: 48 | └───user 49 | ├───USE.js 50 | ├───GET.js 51 | ├───POST.js 52 | └───[userId] // NextJS Format 53 | ├───GET.js 54 | ├───PUT.js 55 | └───DELETE.js 56 | ``` 57 | 58 | ### Step 4: Exporting Route Rules 59 | 60 | In each API file within the directory structure, you can export the allowed request methods. For example, in the `src/api/user/[userId]/DELETE.js` file, you can export the default method for `DELETE`: 61 | 62 | ```javascript 63 | ///file:src/api/user/[userId]/DELETE.js 64 | export default (req, res, next) => { 65 | res.send("DELETE REQUEST"); 66 | }; 67 | ``` 68 | 69 | Similarly, the file names `GET.js` or `POST.js` will be exported as request parameters, such as `/user/:userId/`, following the Next.js/Remix framework. 70 | 71 | ### Step 5: Add Middlewares 72 | 73 | For every route you can export an array for middlewares. This can be used for authentication purposes or any other sort of middleware that you need. 74 | 75 | ```javascript 76 | ///file:src/api/user/[userId]/DELETE.js 77 | import authMiddleware from "..."; 78 | 79 | const USER_DELETE = (req, res, next) => { 80 | res.send("DELETE REQUEST"); 81 | }; 82 | 83 | export default [authMiddleware, USER_DELETE]; 84 | ``` 85 | 86 | ### Step 6: Configure Aliases 87 | 88 | In order to have proper access to the plugin's alias definitions, you need to include `/.api/env.d.ts` in either `src/vite-env.d.ts` or in your `tsconfig.json`. This will allow you to use the alias correctly throughout your project. 89 | 90 | #### Option 1: Add to `vite-env.d.ts` 91 | 92 | In your `src/vite-env.d.ts`, add the following line: 93 | 94 | ```typescript 95 | /// 96 | ``` 97 | 98 | ### Step 7: Run the Server 99 | 100 | Now, you can start the server using ViteJS, and the API routes will be automatically generated based on the directory structure and exported route rules. 101 | 102 | ### Conclusion 103 | 104 | Congratulations! You have successfully enhanced API routing in your ViteJS project using the `vite-plugin-api-routes` plugin. This improves project structure and simplifies configuration, making your development process more efficient. 105 | 106 | Remember to refer to the plugin's documentation for more advanced configuration options and customization possibilities. 107 | 108 | Happy coding! 109 | -------------------------------------------------------------------------------- /packages/plugin/tutorial-legacy.md: -------------------------------------------------------------------------------- 1 | ## Tutorial: Enhancing API Routing in ViteJS with `vite-plugin-api-routes` 2 | 3 | ### Introduction 4 | 5 | In this tutorial, we will learn how to enhance API routing in ViteJS using the `vite-plugin-api-routes` plugin. This plugin improves visibility and project structure in Node.js and Express by mapping the directory structure to route rules. 6 | 7 | ### Prerequisites 8 | 9 | Before we begin, make sure you have the following: 10 | 11 | - Basic knowledge of ViteJS, Node.js, and Express. 12 | - An existing ViteJS project. 13 | 14 | ### Step 1: Installation 15 | 16 | To install the `vite-plugin-api-routes` plugin, run the following command: 17 | 18 | ```bash 19 | yarn add vite-plugin-api-routes 20 | ``` 21 | 22 | ### Step 2: Configuration 23 | 24 | In your ViteJS project, open the `vite.config.ts` file and add the following code: 25 | 26 | ```js 27 | import { defineConfig } from "vite"; 28 | import apiRoutes from "vite-plugin-api-routes"; 29 | 30 | export default defineConfig({ 31 | plugins: [ 32 | apiRoutes({ 33 | // Configuration options go here 34 | }), 35 | ], 36 | }); 37 | ``` 38 | 39 | ### Step 3: Directory Structure 40 | 41 | Next, let's create the API directory structure in your project's source folder (`src/api`). Here's an example structure: 42 | 43 | ```bash 44 | > tree src/api/ 45 | src/api/: 46 | ├───v1 47 | │ │ auth.js 48 | │ │ index.js 49 | │ ├───auth 50 | │ │ login.js 51 | │ │ status.js 52 | │ └───user 53 | │ $userId.js // Remix Format 54 | │ index.js 55 | └───v2 56 | │ user.js 57 | ├───auth 58 | │ login.js 59 | │ status.js 60 | └───user 61 | index.js 62 | [userId].js // NextJS Format 63 | ``` 64 | 65 | ### Step 4: Exporting Route Rules 66 | 67 | In each API file within the directory structure, you can export the allowed request methods. For example, in the `src/api/v1/user/$userId.js` file, you can export the `DELETE` and `PUT` methods: 68 | 69 | ```javascript 70 | ///file:src/api/v1/user/$userId.js 71 | export const DELETE = (req, res, next) => { 72 | res.send("DELETE REQUEST"); 73 | }; 74 | export const PUT = async (req, res, next) => { 75 | res.send("PUT REQUEST"); 76 | }; 77 | ``` 78 | 79 | Similarly, the file names `[userId].js` or `$userId.js` will be exported as request parameters, such as `/user/:userId`, following the Next.js/Remix framework. 80 | 81 | ### Step 5: Add Middlewares 82 | 83 | For every route you can export an array for middlewares. This can be used for authentication purposes or any other sort of middleware that you need. 84 | 85 | ```javascript 86 | //file:src/api/v1/user/$userId.js 87 | 88 | import authMiddleware from '...'; 89 | // or 90 | function authMiddleware(req, res, next) => { 91 | // ... 92 | } 93 | 94 | export default [ 95 | authMiddleware 96 | ]; 97 | ``` 98 | 99 | ### Step 6: Configure Aliases 100 | 101 | In order to have proper access to the plugin's alias definitions, you need to include `/.api/env.d.ts` in either `src/vite-env.d.ts` or in your `tsconfig.json`. This will allow you to use the alias correctly throughout your project. 102 | 103 | #### Option 1: Add to `vite-env.d.ts` 104 | 105 | In your `src/vite-env.d.ts`, add the following line: 106 | 107 | ```typescript 108 | /// 109 | ``` 110 | 111 | 112 | ### Step 7: Run the Server 113 | 114 | Now, you can start the server using ViteJS, and the API routes will be automatically generated based on the directory structure and exported route rules. 115 | 116 | ### Conclusion 117 | 118 | Congratulations! You have successfully enhanced API routing in your ViteJS project using the `vite-plugin-api-routes` plugin. This improves project structure and simplifies configuration, making your development process more efficient. 119 | 120 | Remember to refer to the plugin's documentation for more advanced configuration options and customization possibilities. 121 | 122 | Happy coding! 123 | --------------------------------------------------------------------------------