├── .env.example ├── .gitignore ├── .vercelignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── nodemon.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── server ├── app │ ├── Controllers │ │ ├── Controller.ts │ │ └── Voucher.ts │ └── Helpers │ │ ├── Http.ts │ │ ├── HttpError.ts │ │ └── index.ts ├── index.ts ├── routes │ ├── Api │ │ ├── Voucher.ts │ │ └── index.ts │ ├── Router.ts │ └── index.ts └── utils │ ├── config.ts │ └── cors.ts ├── src ├── App.test.tsx ├── App.tsx ├── components │ ├── 404 │ │ └── index.tsx │ └── Layout │ │ └── index.tsx ├── index.scss ├── index.tsx ├── logo.svg ├── pages │ ├── About │ │ ├── index.tsx │ │ └── styles.scss │ └── Home │ │ ├── index.tsx │ │ └── styles.scss ├── react-app-env.d.ts ├── reportWebVitals.ts ├── routes.tsx ├── setupTests.ts └── utils │ └── config.ts ├── tsconfig.json ├── tsconfig.server.json └── vercel.json /.env.example: -------------------------------------------------------------------------------- 1 | # SERVER 2 | NODE_ENV = development 3 | 4 | # CLIENT 5 | REACT_APP_API_BASEURL=http://localhost:3000/api 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | build-server 4 | .env 5 | .vercel 6 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.example 3 | .vscode 4 | LICENSE 5 | README.md 6 | nodemon.json 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sutan Nasution. 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 | # Monolithic React + Express Boilerplate with TypeScript ❤ 2 | 3 | ## DEMO 4 | - [React App](https://monolith-express-react.sutanlab.id) 5 | - [API Endpoint at `/api`](https://monolith-express-react.sutanlab.id/api/voucher) 6 | 7 | ## Why i made this? 8 | Short Answer: ***Personal Purpose*** 9 | 10 | Long Answer: Because me (or maybe you?) want to build a monorepo/monolith project with Express and React which combines server and frontend in one repository. Of course, it's cost-effective! 11 | 12 | The reason why it's cost-effective are: 13 | - That's right, because it's simpler and can be deployed on [Vercel](https://vercel.com) for free. 14 | - Because `Backend` and `Frontend` only have one `node_modules`. 15 | - Because `Backend` and `Frontend` can share code with each other. 16 | - The irony is, because you're the both `Backend` and `Frontend`. 17 | 18 | ## Setup 19 | - Clone repository `$ git clone https://github.com/sutanlab/monolith-express-react.git` 20 | 21 | - Install depedencies 22 | ```bash 23 | # with npm 24 | $ npm install 25 | 26 | # or with yarn 27 | $ yarn install 28 | ``` 29 | 30 | - Run server in development mode 31 | ```bash 32 | $ npm run dev 33 | # or 34 | $ yarn dev 35 | ``` 36 | 37 | - Build optimize production mode 38 | ```bash 39 | $ npm run build 40 | # or 41 | $ yarn build 42 | ``` 43 | 44 | - Start server in production mode 45 | ```bash 46 | $ npm start 47 | # or 48 | $ yarn start 49 | ``` 50 | 51 | - Deploy to vercel 52 | ```bash 53 | $ npm run deploy 54 | # or 55 | $ yarn deploy 56 | ``` 57 | ## LICENSE 58 | MIT 59 | 60 | ## Support Me 61 | ### Global 62 | [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/gadingnst) 63 | ### Indonesia 64 | - [Trakteer](https://trakteer.id/gadingnst) 65 | - [Karyakarsa](https://karyakarsa.com/gadingnst) 66 | 67 | --- 68 | 69 | Copyright ©2021 by Sutan Gading Fadhillah Nasution 70 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts", 3 | "ignore": ["*.spec.ts"], 4 | "exec": "ts-node --project tsconfig.server.json server/index.ts" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monolith-express-react", 3 | "version": "1.0.0", 4 | "description": "Monolithic Express + React Boilerplate with TypeScript and Vercel ❤", 5 | "private": true, 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=production node build-server/index.js", 8 | "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", 9 | "build": "npm run build:client && npm run build:server", 10 | "deploy": "vercel --prod", 11 | "dev:server": "nodemon", 12 | "dev:client": "cross-env PORT=5000 react-scripts start", 13 | "build:server": "tsc --project tsconfig.server.json", 14 | "build:client": "react-scripts build", 15 | "test:client": "react-scripts test", 16 | "eject:client": "react-scripts eject", 17 | "cleanup": "rm -rf build/api", 18 | "postbuild": "react-snap; npm run cleanup" 19 | }, 20 | "author": "Sutan Nasution ", 21 | "license": "MIT", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/sutanlab/monolith-express-react.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/sutanlab/monolith-express-react/issues" 28 | }, 29 | "reactSnap": { 30 | "source": "build", 31 | "minifyHtml": { 32 | "collapseWhitespace": false, 33 | "removeComments": true 34 | }, 35 | "puppeteerExitOnPageError": false, 36 | "puppeteerArgs": [ 37 | "--no-sandbox", 38 | "--disable-setuid-sandbox" 39 | ] 40 | }, 41 | "eslintConfig": { 42 | "extends": [ 43 | "react-app", 44 | "react-app/jest" 45 | ] 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "dependencies": { 60 | "cors": "^2.8.5", 61 | "cross-env": "^7.0.3", 62 | "dotenv": "^8.2.0", 63 | "express": "^4.17.1", 64 | "react": "^17.0.1", 65 | "react-dom": "^17.0.1", 66 | "react-router-dom": "^5.2.0", 67 | "react-snap": "^1.23.0", 68 | "vercel": "^21.2.3", 69 | "web-vitals": "^1.1.0" 70 | }, 71 | "devDependencies": { 72 | "@testing-library/jest-dom": "^5.11.9", 73 | "@testing-library/react": "^11.2.5", 74 | "@testing-library/user-event": "^12.7.1", 75 | "@types/cors": "^2.8.10", 76 | "@types/express": "^4.17.11", 77 | "@types/jest": "^26.0.20", 78 | "@types/node": "^12.20.4", 79 | "@types/react": "^17.0.2", 80 | "@types/react-dom": "^17.0.1", 81 | "@types/react-router-dom": "^5.1.7", 82 | "concurrently": "^5.3.0", 83 | "node-sass": "^5.0.0", 84 | "nodemon": "^2.0.7", 85 | "react-scripts": "4.0.2", 86 | "ts-node": "^9.1.1", 87 | "typescript": "^4.1.5" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadingnst/monolith-express-react/7f9e1940e128ce22b6d892c4ca14eb7dc4bc05eb/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadingnst/monolith-express-react/7f9e1940e128ce22b6d892c4ca14eb7dc4bc05eb/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadingnst/monolith-express-react/7f9e1940e128ce22b6d892c4ca14eb7dc4bc05eb/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /server/app/Controllers/Controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express' 2 | import Http, { HttpResponse } from '../Helpers/Http' 3 | import HttpError from '../Helpers/HttpError' 4 | 5 | export default class Controller { 6 | protected send(res: Response, data: HttpResponse): Response { 7 | return Http.send(res, data) 8 | } 9 | 10 | protected setError(code: number, msg: string): void { 11 | throw new HttpError(code, msg) 12 | } 13 | 14 | protected handleError(req: Request, res: Response, error: Error): Response { 15 | return HttpError.handle(req, res, error) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/app/Controllers/Voucher.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express' 2 | import Controller from './Controller' 3 | 4 | class VoucherController extends Controller { 5 | // NOTE: Please use arrow function to avoid ambiguous of "this" 6 | 7 | public get = (req: Request, res: Response) => { 8 | // dummy data. only for demo 9 | const payload: any[] = [ 10 | { id: 1, name: 'Indomie', price: 2100 }, 11 | { id: 2, name: 'Roti', price: 3000 }, 12 | { id: 3, name: 'Aqua', price: 500 }, 13 | { id: 4, name: 'Telur', price: 2500 }, 14 | this.test() 15 | ] 16 | try { 17 | this.send(res, { 18 | code: 200, 19 | message: `OK`, 20 | payload 21 | }) 22 | } catch (err) { 23 | this.handleError(req, res, err) 24 | } 25 | } 26 | 27 | public create = (req: Request, res: Response) => { 28 | try { 29 | this.send(res, { 30 | code: 201, 31 | message: `Created` 32 | }) 33 | } catch (err) { 34 | this.handleError(req, res, err) 35 | } 36 | } 37 | 38 | public testHandleHttpError = (req: Request, res: Response) => { 39 | try { 40 | // make sure throwing error with Http helper works 41 | this.setError(400, 'Bad Request') 42 | } catch (err) { 43 | this.handleError(req, res, err) 44 | } 45 | } 46 | 47 | private test = () => { 48 | return { id: 5, name: 'Beras', price: 9000 } 49 | } 50 | } 51 | 52 | export default new VoucherController() 53 | -------------------------------------------------------------------------------- /server/app/Helpers/Http.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express' 2 | 3 | export interface HttpResponse { 4 | code: number, 5 | message: string, 6 | error?: boolean, 7 | payload?: T 8 | } 9 | 10 | export default class Http { 11 | public static send(res: Response, data: HttpResponse): Response { 12 | return res.status(data.code).send({ error: false, ...data }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/app/Helpers/HttpError.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express' 2 | 3 | export default class HttpError extends Error { 4 | constructor(code: number, message: string) { 5 | super(JSON.stringify({ code, message, error: true })) 6 | Object.setPrototypeOf(this, this.constructor.prototype) 7 | } 8 | 9 | public static handle(req: Request, res: Response, err: Error): Response { 10 | if (err.message.includes('code')) { 11 | const error = JSON.parse(err.message) 12 | console.error(error) 13 | return res.status(error.code).send(error) 14 | } 15 | console.error(err) 16 | return res.status(500).send({ 17 | code: 500, 18 | message: `Internal server error!`, 19 | error: true 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/app/Helpers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Http } from './Http' 2 | export { default as HttpError } from './HttpError' 3 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import Express, { Application } from 'express' 2 | import Routes from './routes' 3 | import { PORT } from './utils/config' 4 | import Cors from './utils/cors' 5 | 6 | export default class Server { 7 | private application: Application 8 | private port: number|string 9 | 10 | constructor() { 11 | this.port = PORT 12 | this.application = Express() 13 | } 14 | 15 | private plugins() { 16 | this.application.use(Express.urlencoded({ extended: true })) 17 | this.application.use(Express.json()) 18 | this.application.use(Cors()) 19 | this.application.use(Routes) 20 | } 21 | 22 | public run() { 23 | try { 24 | this.plugins() 25 | this.application.listen(this.port, () => { 26 | console.log(`> Server running on http://localhost:${this.port}`) 27 | }) 28 | } catch (err) { 29 | console.error(err) 30 | process.exit(1) 31 | } 32 | } 33 | } 34 | 35 | new Server().run() 36 | -------------------------------------------------------------------------------- /server/routes/Api/Voucher.ts: -------------------------------------------------------------------------------- 1 | import VoucherController from '../../app/Controllers/Voucher' 2 | import Router from '../Router' 3 | 4 | class VoucherRoute extends Router { 5 | public baseRoute = '/voucher' 6 | 7 | public routes() { 8 | this.router.get('/', VoucherController.get) 9 | this.router.post('/', VoucherController.create) 10 | this.router.get('/demo-error', VoucherController.testHandleHttpError) 11 | } 12 | } 13 | 14 | export default new VoucherRoute() -------------------------------------------------------------------------------- /server/routes/Api/index.ts: -------------------------------------------------------------------------------- 1 | import Router from '../Router' 2 | import VoucherRoute from './Voucher' 3 | 4 | class ApiRoute extends Router { 5 | public baseRoute = '/api' 6 | 7 | public routes() { 8 | this.router.use(VoucherRoute.baseRoute, VoucherRoute.router) 9 | } 10 | } 11 | 12 | export default new ApiRoute() 13 | -------------------------------------------------------------------------------- /server/routes/Router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | 3 | abstract class BaseRouter { 4 | public router: Router 5 | public abstract baseRoute: string 6 | 7 | constructor() { 8 | this.router = Router() 9 | this.routes() 10 | } 11 | 12 | protected abstract routes(): void 13 | } 14 | 15 | export default BaseRouter 16 | -------------------------------------------------------------------------------- /server/routes/index.ts: -------------------------------------------------------------------------------- 1 | import Path from 'path' 2 | import Express from 'express' 3 | import Router from './Router' 4 | import ApiRoute from './Api' 5 | import { IS_PRODUCTION, CLIENT_BUILD_PATH } from '../utils/config' 6 | 7 | class Routes extends Router { 8 | public baseRoute = '/' 9 | 10 | public routes() { 11 | this.router.use(ApiRoute.baseRoute, ApiRoute.router) 12 | if (IS_PRODUCTION) { 13 | this.router.use(Express.static(CLIENT_BUILD_PATH)) 14 | this.router.use('*', (_, res) => { 15 | res.status(404).sendFile(Path.resolve(CLIENT_BUILD_PATH, 'index.html')) 16 | }) 17 | } 18 | } 19 | } 20 | 21 | export default new Routes().router -------------------------------------------------------------------------------- /server/utils/config.ts: -------------------------------------------------------------------------------- 1 | import Dotenv from 'dotenv' 2 | 3 | Dotenv.config() 4 | 5 | export const { 6 | NODE_ENV = 'production', 7 | PORT = 3000 8 | } = process.env 9 | 10 | export const IS_PRODUCTION = NODE_ENV === 'production' 11 | export const CLIENT_BUILD_PATH = `${__dirname}/../../build` 12 | -------------------------------------------------------------------------------- /server/utils/cors.ts: -------------------------------------------------------------------------------- 1 | import Cors, { CorsOptions } from 'cors' 2 | import { IS_PRODUCTION } from './config' 3 | 4 | const productionWhiteList: string[] = [] 5 | 6 | const developmentWhiteList: string[] = [ 7 | 'http://localhost', 8 | 'http://localhost:3000', 9 | 'http://localhost:5000', 10 | 'http://localhost:8080' 11 | ] 12 | 13 | export const whiteListDomain = IS_PRODUCTION 14 | ? productionWhiteList 15 | : developmentWhiteList 16 | 17 | export const corsOptions: CorsOptions = { 18 | origin: (origin?, callback?) => { 19 | if (!origin || whiteListDomain.indexOf(origin) !== -1) { 20 | callback(null, true) 21 | } else { 22 | callback(new Error('Not Allowed to access the request!')) 23 | } 24 | } 25 | } 26 | 27 | const CorsInstance = (newOptions: CorsOptions = {}) => 28 | Cors({ ...corsOptions, ...newOptions }) 29 | 30 | export default CorsInstance -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import Routes from './routes' 2 | 3 | export default Routes 4 | -------------------------------------------------------------------------------- /src/components/404/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react' 2 | 3 | const NotFound: FunctionComponent = () => { 4 | return ( 5 |
6 |

404 Not Found

7 |
8 | ) 9 | } 10 | 11 | export default NotFound 12 | -------------------------------------------------------------------------------- /src/components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, Fragment, Suspense } from 'react' 2 | 3 | const Layout: FunctionComponent = ({ children }) => ( 4 | 5 | Loading ...}> 6 | {children} 7 | 8 | 9 | ) 10 | 11 | export default Layout -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.scss'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | globalThis.React = React 8 | 9 | const root = document.getElementById('root') 10 | 11 | const AppWrapper = () => ( 12 | 13 | 14 | 15 | ) 16 | 17 | if (root?.hasChildNodes()) { 18 | ReactDOM.hydrate(, root) 19 | } else { 20 | ReactDOM.render(, root) 21 | } 22 | 23 | // If you want to start measuring performance in your app, pass a function 24 | // to log results (for example: reportWebVitals(console.log)) 25 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 26 | reportWebVitals() 27 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/About/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react' 2 | import { Link } from 'react-router-dom' 3 | import logo from '../../logo.svg' 4 | import './styles.scss' 5 | 6 | const About: FunctionComponent = () => { 7 | return ( 8 |
9 |
10 | logo 11 |

12 | This is About Page 13 |

14 | 15 | Go To Home 16 | 17 |
18 |
19 | ) 20 | } 21 | 22 | export default About 23 | -------------------------------------------------------------------------------- /src/pages/About/styles.scss: -------------------------------------------------------------------------------- 1 | .text-about { 2 | color: aquamarine; 3 | } -------------------------------------------------------------------------------- /src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useEffect } from 'react' 2 | import { Link } from 'react-router-dom' 3 | import logo from '../../logo.svg' 4 | import './styles.scss' 5 | import { API_BASEURL } from '../../utils/config' 6 | 7 | const Home: FunctionComponent = () => { 8 | useEffect(() => { 9 | // make sure that ENV Variables works 10 | console.log({ API_BASEURL }) 11 | window.fetch(`${API_BASEURL}/voucher`) 12 | .then(response => response.json()) 13 | .then(data => { 14 | console.log({ data }) 15 | }) 16 | .catch((err) => { 17 | console.error(err) 18 | }) 19 | }, []) 20 | return ( 21 |
22 |
23 | logo 24 |

25 | This is Home page.
26 | (For make sure the API works, please check browser log.) 27 |

28 | 29 | Go To About 30 | 31 | 32 | Check Deployed Dummy API 33 | 34 |
35 |
36 | ) 37 | } 38 | 39 | export default Home 40 | -------------------------------------------------------------------------------- /src/pages/Home/styles.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react' 2 | import { BrowserRouter, Route, Switch } from 'react-router-dom' 3 | import Layout from './components/Layout' 4 | import Error404 from './components/404' 5 | 6 | import Home from './pages/Home' 7 | import About from './pages/About' 8 | 9 | const Routes: FunctionComponent = () => ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | 21 | export default Routes -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | export const { 2 | REACT_APP_API_BASEURL: API_BASEURL = '/api' 3 | } = process.env 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "build-server", 6 | "downlevelIteration": true, 7 | "noEmit": false 8 | }, 9 | "include": ["server"] 10 | } 11 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "build-server/index.js", 6 | "use": "@now/node-server" 7 | }, 8 | { 9 | "src": "package.json", 10 | "use": "@now/static-build", 11 | "config": { 12 | "distDir": "build" 13 | } 14 | } 15 | ], 16 | "routes": [ 17 | { 18 | "src": "/api/(.*)", 19 | "dest": "build-server/index.js" 20 | }, 21 | { "handle": "filesystem" }, 22 | { 23 | "src": "/(.*)", 24 | "status": 404, 25 | "dest": "build/404.html" 26 | } 27 | ] 28 | } 29 | --------------------------------------------------------------------------------