├── .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 ];
--------------------------------------------------------------------------------