loading...

Setting up fullstack TypeScript app

nuclight profile image Konstantin Alikhanov ・6 min read

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

Goto http://localhost:3000

Full code can be found here.

Have a nice day!

Posted on by:

Discussion

markdown guide
 

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.