As you know, create-react-app
is a grate tool for scaffolding React.JS applications. It support TypeScript. But it only configures frontend part of app. If you need to set up backend too, you may run in to troubles.
In this article I will describe my approach of scaffolding fullstack TypeScript apps.
Basics
First of all lets init our new project folder. I'm gonna use npm
.
$ mkdir my-fullstack-typescript-app
$ cd my-fullstack-typescript-app
$ npm init -y
Now we should install typescript
package as development dependency.
$ npm i -D typescript
We will have two different tsconfig.json
files, one for backend, second for frontend.
Let's generate one from backend.
$ npx tsc --init
This will create tsconfig.json
file in our project root directory. I'm gonna to update some fields in it.
Open ./tsconfig.json
in your favorite editor and change compilerOptions.target
to "es6"
.
Our source code will be in directory ./src
and compiled code in directory ./dist
. Uncomment and change options compilerOptions.root
and compilerOptions.outDir
to "./src"
and "./dist"
respectively.
Also, I gonna uncomment option compilerOptions.sourceMap
to allow debugging of compiled code.
Now your ./tsconfig.json
should look like this:
{
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
}
}
Note: I have removed all other commented fields to keep code short.
Backend
Ok. Let's writing simple backend.
We need to install type definitions for node.js to tell TypeScript information about node.js standrard library.
$ npm i -D @types/node
Also I gonna use express
as backend framework and ejs
as template engine, so let's install them too.
$ npm i express
$ npm i -D @types/express
$ npm i ejs
Now we can start coding.
Let's create ./src
dir and then ./src/config.ts
file.
In this file I gonna store some configuration variables for our app.
By now, let's put there just single line of code:
export const SERVER_PORT = parseInt(process.env.SERVER_PORT || "3000");
Ok. Now we can write our web module.
I gonna place whole logic of web module in ./src/web
dir.
Create file ./src/web/web.ts
with content of our web module:
import express from "express";
import http from "http";
import path from "path";
// Express app initialization
const app = express();
// Template configuration
app.set("view engine", "ejs");
app.set("views", "public");
// Static files configuration
app.use("/assets", express.static(path.join(__dirname, "frontend")));
// Controllers
app.get("/*", (req, res) => {
res.render("index");
});
// Start function
export const start = (port: number): Promise<void> => {
const server = http.createServer(app);
return new Promise<void>((resolve, reject) => {
server.listen(port, resolve);
});
};
Two things you can notice here. First — we need view directory ./public
. Second — we need static files directory frontend
.
Let's create ./public
dir (in root of our project) and place there file index.ejs
with content:
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Fullstack TypeScript App</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<div id="root"></div>
<script src="/assets/vendors~main.chunk.js"></script>
<script src="/assets/main.bundle.js"></script>
</body>
</html>
Here you can see, that we have two script
tags, targeting frontend code bundles. We will use Webpack to build frontend budles.
Path to frontend
is tricky one. Our frontend code will be stored in ./src/web/frontend
dir. But compiled bundle appear in ./dist/web/frontend
. We will set up frontend in a minute, but first let's finish backend.
I like to treat with complex module like with a single, so let's create file ./src/web/index.ts
with a single line:
export * from "./web";
And we have done with web module.
The last thing left here is to create entry point file ./src/main.ts
with following content:
import {SERVER_PORT} from "./config";
import * as web from "./web";
async function main() {
await web.start(SERVER_PORT);
console.log(`Server started at http://localhost:${SERVER_PORT}`);
}
main().catch(error => console.error(error));
Our backend is done 🥳. Let's compile it.
Open ./package.json
file and add some npm scripts:
"build:backend": "tsc",
"start": "./dist/main.js"
So your ./package.json
file should look like this:
{
"name": "my-fullstack-typescript-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build:backend": "tsc",
"start": "node ./dist/main.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.16.1",
"@types/node": "^11.9.6",
"typescript": "^3.3.3333"
},
"dependencies": {
"ejs": "^2.6.1",
"express": "^4.16.4"
}
}
Now you can chat that this part works:
$ npm run build:backend
$ npm start
But if we visit http://localhost:3000 we will see just black page.
Frontend
By now our project structure looks like:
.
├── dist
│ ├── web
│ │ ├── index.js
│ │ ├── index.js.map
│ │ ├── web.js
│ │ └── web.js.map
│ ├── config.js
│ ├── config.js.map
│ ├── main.js
│ └── main.js.map
├── public
│ └── index.ejs
├── src
│ ├── web
│ │ ├── index.ts
│ │ └── web.ts
│ ├── config.ts
│ └── main.ts
├── package-lock.json
├── package.json
└── tsconfig.json
We are ready to create ./src/web/frontend
dir to store our frontend code.
Important thing here: we are using TypeScript compiler with configuration in ./tsconfig.json
to compile backend code. But for frontend we will use Webpack and TypeScript configuration in file ./src/web/frontend/tsconfig.json
.
So let's create ./src/web/frontend
dir and initialize ./src/web/frontend/tsconfig.json
file.
$ mkdir ./src/web/frontend
$ cd ./src/web/frontend
$ npx tsc --init
We end up with a tsconfig.json
file in ./src/web/frontend/
.
Let's open in and make some changes.
Again, set compilerOptions.target
to "es6"
.
Set compilerOptions.module
to "esnext"
.
Uncomment option compilerOptions.sourceMap
to allow debugging of frontend bundles.
Uncomment and set compilerOptions.jsx
to "react"
.
Your ./src/web/frontend/tsconfig.json
should look like:
{
"compilerOptions": {
"target": "es6",
"module": "esnext",
"sourceMap": true,
"jsx": "react",
"strict": true,
"esModuleInterop": true
}
}
Note: we do not specify here compilerOptions.rootDir
and compilerOptions.outDir
. Files resolution will be done by Webpack.
Now we need to make backend compiler to ignore frontend files.
To do so, we need to add two options to ./tsconfig.json
:
"include": ["./src"],
"exclude": ["./src/web/frontend"]
Your ./tsconfig.json
should look like:
{
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
},
"include": ["./src"],
"exclude": ["./src/web/frontend"]
}
Our frontend entry point will be ./src/web/frontend/main.tsx
:
import React, {useState} from "react";
import ReactDOM from "react-dom";
import "./style.css";
const App = () => {
const [counter, setCounter] = useState(0);
return (
<div className="App">
<h1>{counter}</h1>
<button onClick={() => setCounter(c + 1)}>Press me</button>
</div>
)
};
ReactDOM.render(
<App/>,
document.getElementById("root"),
);
This is a very simple React.JS app.
Let style it a little bit with ./src/web/frontend/style.css
:
.App {
margin: 30px auto;
max-width: 320px;
padding: 2em;
border: 1px solid silver;
border-radius: 1em;
text-align: center;
}
Let's install needed packages:
$ npm i react react-dom
$ npm i -D @types/react @types/react-dom
For building frontend I gonna use Webpack and ts-loader
package.
Let's install all of needed stuff:
$ npm i -D webpack webpack-cli ts-loader style-loader css-loader source-map-loader
Now we need to configure Webpack. Let's create ./webpack.config.js
with following content:
module.exports = {
mode: "development",
entry: {
main: "./src/web/frontend/main.tsx",
},
output: {
filename: "[name].bundle.js",
chunkFilename: '[name].chunk.js',
path: __dirname + "/dist/web/frontend",
publicPath: "/assets/"
},
// Enable sourcemaps for debugging webpack's output.
devtool: "source-map",
resolve: {
// Add '.ts' and '.tsx' as resolvable extensions.
extensions: [".ts", ".tsx", ".js"]
},
module: {
rules: [
// All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.
{
test: /\.tsx?$/,
loader: "ts-loader",
},
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
{enforce: "pre", test: /\.js$/, loader: "source-map-loader"},
{
test: /\.css$/,
use: [{loader: "style-loader"}, {loader: "css-loader"}]
},
]
},
optimization: {
splitChunks: {
chunks: "all"
},
usedExports: true
},
};
And we have done!
Last thing left is to add npm script to ./package.json
file to build frontend:
"build:frontend": "webpack"
Now you can test it:
$ npm run build:backend
$ npm run build:frontend
$ npm start
Full code can be found here.
Have a nice day!
Top comments (2)
Thanks so much for this. I've been scouring the net and trying out many different setups, this is by far the best I've found.
Fantastic