├── .gitignore ├── Dockerfile ├── src ├── views │ ├── index.html │ └── index.tsx └── app.ts ├── tsconfig.json ├── tern-starter-deployment.yml ├── Jenkinsfile ├── package.json ├── README.md └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | .idea/ 4 | 5 | dist/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:17-alpine3.14 2 | WORKDIR /app 3 | COPY package.json package-lock.json /app/ 4 | RUN npm ci --only=production && npm cache clean --force 5 | COPY ./dist/ /app 6 | CMD node bundle-back.js 7 | EXPOSE 3000 -------------------------------------------------------------------------------- /src/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TERN Starter 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src/", 4 | "outDir": "./dist/", 5 | "sourceMap": true, 6 | "noImplicitAny": true, 7 | "module": "commonjs", 8 | "target": "es5", 9 | "jsx": "react", 10 | "lib": ["es2015", "dom"], 11 | "skipLibCheck": true 12 | }, 13 | "include": [ 14 | "./src/**/*" 15 | ] 16 | } -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import {Request, Response} from 'express'; 3 | import * as path from 'path'; 4 | 5 | const app = express(); 6 | 7 | app.use(express.static(path.join(__dirname, '/'))); 8 | 9 | app.get('/', (req: Request, res: Response) => { 10 | res.sendFile(path.join(__dirname + '/index.html')); 11 | }); 12 | app.get('/ApiTest', (req: Request, res: Response) => { 13 | res.send('The node server says Hello World!!!'); 14 | }); 15 | 16 | app.listen(3000, () => console.log('Listening on port 3000')); 17 | 18 | process.on( 'SIGINT', () => { 19 | console.log( "\nGracefully shutting down from SIGINT (Ctrl-C)" ); 20 | process.exit(); 21 | }); -------------------------------------------------------------------------------- /src/views/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom'; 3 | import axios from 'axios'; 4 | 5 | class App extends React.Component { 6 | constructor(props: any) { 7 | super(props); 8 | this.state = { 9 | message: 'No response from server!' 10 | } 11 | } 12 | apiTest() { 13 | axios.get('/ApiTest').then(response => { 14 | this.setState({message: response.data}); 15 | }) 16 | } 17 | componentDidMount(): void { 18 | this.apiTest(); 19 | } 20 | render() { 21 | return(
{this.state.message}
) 22 | } 23 | } 24 | 25 | ReactDOM.render( 26 | , 27 | document.getElementById("app") 28 | ); -------------------------------------------------------------------------------- /tern-starter-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tern-starter-deployment 5 | labels: 6 | app: tern-starter 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: tern-starter 12 | template: 13 | metadata: 14 | labels: 15 | app: tern-starter 16 | spec: 17 | containers: 18 | - name: tern-starter 19 | image: bshandley/tern-starter:latest 20 | ports: 21 | - containerPort: 3000 22 | --- 23 | apiVersion: v1 24 | kind: Service 25 | metadata: 26 | name: tern-starter 27 | spec: 28 | selector: 29 | app: tern-starter 30 | ports: 31 | - protocol: TCP 32 | port: 3000 33 | targetPort: 3000 34 | nodePort: 30081 35 | type: NodePort -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | node { 2 | def app 3 | 4 | stage('Clone repository') { 5 | checkout scm 6 | } 7 | 8 | stage('Build application') { 9 | sh 'npm install' 10 | sh 'npm run-script build' 11 | } 12 | 13 | stage('Build Docker image') { 14 | app = docker.build("bshandley/tern-starter") 15 | } 16 | 17 | stage('Test image') { 18 | app.inside { 19 | sh 'echo "Tests passed"' 20 | } 21 | } 22 | 23 | stage('Push image') { 24 | docker.withRegistry('https://registry.hub.docker.com', 'docker-hub-credentials') { 25 | app.push("${env.BUILD_NUMBER}") 26 | app.push("latest") 27 | } 28 | } 29 | 30 | stage ('Update k8 cluster') { 31 | sh 'kubectl rollout restart deployment tern-starter-deployment' 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tern-stack-starter", 3 | "version": "2.0.1", 4 | "description": "TERN Stack Starter", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack --config webpack.config.js --watch", 8 | "build": "webpack --config webpack.config.js" 9 | }, 10 | "author": "Bradley S Handley", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^0.26.0", 14 | "express": "^4.17.3", 15 | "path": "^0.12.7", 16 | "react": "^17.0.2", 17 | "react-dom": "^17.0.2" 18 | }, 19 | "devDependencies": { 20 | "@types/axios": "^0.14.0", 21 | "@types/express": "^4.17.13", 22 | "@types/react": "^17.0.39", 23 | "@types/react-dom": "^17.0.11", 24 | "awesome-typescript-loader": "^5.2.1", 25 | "file-loader": "^6.2.0", 26 | "nodemon": "^2.0.15", 27 | "nodemon-webpack-plugin": "^4.7.1", 28 | "npm-run-all": "^4.1.5", 29 | "rimraf": "^3.0.2", 30 | "source-map-loader": "^3.0.1", 31 | "typescript": "^4.5.5", 32 | "webpack": "^5.69.1", 33 | "webpack-cli": "^4.9.2", 34 | "webpack-node-externals": "^3.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TERN Half-Stack Starter 2 | **T** Typescript 3 | **E** Expressjs 4 | **R** Reactjs 5 | **N** NodeJs 6 | 7 | React/Typescript frontend with Express/Node backend in a single project. There is a single tsconfig file and webpack.config file for both frontend and backend. 8 | 9 | 10 | 11 | # Why? 12 | Keeping up with separate projects for front and backend is tedious, particularly if you're just trying to prototype. 13 | This stack (hopefully) makes it easy to jump in and start coding, without a lot of overhead. 14 | 15 | **Do not run this code in production environments. This is for POCs and prototyping only.** 16 | 17 | # Other Uses 18 | I have included various other files for containerization, CI/CD pipelines, and Kubernetes testing. You may safely delete `Dockerfile`, `Jenkinsfile`, and `tern-starter-deployment.yml` without breaking anything. Feel free to use and modify these files for your own needs. 19 | 20 | # Getting Started 21 | After installing with `npm install` just run 22 | `npm start` 23 | to build and run the application. The Node server is app.ts, while the React frontend is index.tsx. All changes are monitored and repackaged on save using nodemon. The default port for the Node server is 3000. 24 | 25 | For the Docker file, just run `docker build -t tern-starter .` to build the image and something like `docker run -p 3000:3000 tern-starter` to get it up and running. -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | const webpack = require('webpack'); 4 | const NodemonPlugin = require( 'nodemon-webpack-plugin' ); 5 | 6 | const frontConfig = { 7 | entry: [ 8 | __dirname + "/src/views/index.html", 9 | "./src/views/index.tsx" 10 | ], 11 | output: { 12 | filename: "bundle.js", 13 | path: __dirname + "/dist" 14 | }, 15 | 16 | // Enable sourcemaps for debugging webpack's output. 17 | devtool: "eval-source-map", 18 | 19 | resolve: { 20 | // Add '.ts' and '.tsx' as resolvable extensions. 21 | extensions: [".ts", ".tsx", ".js", ".json"] 22 | }, 23 | 24 | module: { 25 | rules: [ 26 | // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. 27 | { test: /\.tsx?$/, use: "awesome-typescript-loader" }, 28 | 29 | // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. 30 | { enforce: "pre", test: /\.js$/, use: "source-map-loader" }, 31 | 32 | { 33 | test: /\.html/, 34 | use: 'file-loader?name=[name].[ext]', 35 | }, 36 | ] 37 | }, 38 | 39 | // When importing a module whose path matches one of the following, just 40 | // assume a corresponding global variable exists and use that instead. 41 | // This is important because it allows us to avoid bundling all of our 42 | // dependencies, which allows browsers to cache those libraries between builds. 43 | /*externals: { 44 | "react": "React", 45 | "react-dom": "ReactDOM" 46 | }*/ 47 | }; 48 | 49 | const backConfig = { 50 | target: "node", 51 | entry: { 52 | app: ["./src/app.ts"] 53 | }, 54 | output: { 55 | path: __dirname + "/dist", 56 | filename: "bundle-back.js" 57 | }, 58 | plugins: [ 59 | new NodemonPlugin(), // Dong 60 | ], 61 | // Enable sourcemaps for debugging webpack's output. 62 | devtool: "source-map", 63 | 64 | resolve: { 65 | // Add '.ts' and '.tsx' as resolvable extensions. 66 | extensions: [".ts", ".tsx", ".js", ".json"] 67 | }, 68 | module: { 69 | rules: [ 70 | // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. 71 | { test: /\.tsx?$/, use: "awesome-typescript-loader" }, 72 | 73 | // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. 74 | { enforce: "pre", test: /\.js$/, use: "source-map-loader" } 75 | ] 76 | }, 77 | externals: [nodeExternals()], 78 | node: { 79 | __dirname: false 80 | } 81 | }; 82 | 83 | module.exports = [ frontConfig, backConfig ]; --------------------------------------------------------------------------------