loading...
Cover image for Build Instagram Using TypeScript, Node, Express and Vue - Part 1

Build Instagram Using TypeScript, Node, Express and Vue - Part 1

calvintwr profile image calvintwr Updated on ・9 min read

This is tutorial 1 of a 5-part tutorial, but each tutorial can be read in isolation to learn various aspects Node+Express+TypeScript+Vue API/Vue web app set up.

At the end of this 5-part tutorial, you will learn to build an app like this:

Web app tutorial using Node, Express and Vue in TypeScript

Looking to learn mobile/desktop apps? The skills and concepts here are fundamental and re-usable for mobile apps (NativeScript) or desktop apps (Electron). I may cover them as a follow-on.

Navigation to other parts (you are at part 1)

  1. Setting up Node and Express API with TypeScript
  2. Setting up VueJs with TypeScript
  3. Setting up Postgres with Sequelize ORM
  4. Basic Vue templating and interaction with API
  5. Advanced Vue templating and image uploading to Express

Introduction

All good apps should start from a rock-solid base, which is what this tutorial about, illustrated through building a very simple photo sharing app, instead of a Todo (which really doesn't show much). Through these tutorials, you will learn TypeScript, Node, Express and VueJS, using versions as bleeding edge as it can get at the time of this post (some pre-releases where practicable).

*Sadly, Deno was considered but is still too early and to use. However, because of API/view best practice that this tutorial follows, when the time comes, you are likely able to switch to Deno and reuse much of your API codebase. You will re-use all your view coding as it is not coupled to the API (which also means you can develop IOS or Android apps to consume the API as well).

To be completely honest, Instagram can't be built in a single tutorial, so admittedly the title of this post is an exaggeration, but we will call this project "Basicgram".

Get your repo

You can start building by cloning and checking out tutorial-part1 branch:

git clone https://github.com/calvintwr/basicgram.git
git checkout tutorial-part1

Folder Structure

Folders will be split into "api", which will run a Node+Express set up, and "view", which will run a Vue+Webpack set up.

Express/VueJS Folder Structure

Get started - Installing Express (API engine)

npx express-generator --view=hbs

I opted for Handlebars (hbs) as the view engine because it looks like HTML and is most convenient. Nonetheless, it's really not important as we will only be using Express for API service.

We will use the latest Express 5.0 (pre-release) and update all module versions, so edit the package.json file:

{
  "name": "api",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "cookie-parser": "~1.4.5",
    "debug": "~4.1.1",
    "express": "~5.0.0-alpha.8",
    "hbs": "~4.1.1",
    "http-errors": "~1.7.3",
    "morgan": "~1.10.0"
  }
}

Fire it up and see if it all works

npm install
npm start

Go to localhost:3000 and express should greet you.
Express Welcome Page

Express Routings

One of the first Express things you want to get is express-routemagic, which automatically require all our routes instead of declaring them file by file (you will see huge Express apps and their tower of routing codes which doesn't make sense). So just get routemagic, problem solved.

npm install express-routemagic --save

We will replace import indexRouter and usersRouter:

var indexRouter = require('./routes/index')
var usersRouter = require('./routes/users')
app.use('/', indexRouter)
app.use('/users', usersRouter)

With:

const Magic = require('express-routemagic')
Magic.use(app, { invokerPath: __dirname }) // need `invokerPath` because we shifting Express into a `src` folder.

That's it, you will never need to worry about routings. Let's move on.

Converting to TypeScript

TypeScript provides quite a lot of useful features to build better code. You can google its benefits. It has its downsides too, especially being more tedious and having to deal with non-typescript packages (with some even resisting to port themselves to TypeScript syntax). Through this tutorial, figuring how to convert some JS syntax to TypeScript were either painful or close to impossible. But well, we soldier on.

Since it needs to compile to JS at runtime for Node, we will need a few steps to get there.

1. Pack your Express into a "src" folder like this:

Express TypeScript File Structure

And also notice that "app.js" is renamed to "app.ts". We will start with this and leave the rest alone for now. Baby steps.

Tip: It is OK to have mixed codebase. Not everything needs to be in TypeScript.

2. Install TypeScript package and set up configurations

Install TypeScript (note: all npm commands are to run in the basicgram/api folder. api and view are technically two different apps. If you run npm in basicgram, you will mixed their node_modules and other configurations up.)

Setting up the TypeScript compiler

npm install typescript --save-dev

Set up the tsc command in package.json:

"script": {
    "start": "node ./bin/www", // this came default with express, but we will change it later.
    "tsc": "tsc"
}

And initialise tsc which will generate a configuration file:

npx tsc --init

tsconfig.json will now appear in basicgram/api. This controls the compiler's behaviour. There's generally 2 default behaviours we want to change:

  1. TSC by default outputs ES5, which is really unnecessary for Node, being a server-side runtime (if what is stopping you from upgrading Node are your old apps, see Node Version Manager).

  2. It will search compile all .ts files inside of basicgram/api and produce .js alongside it, which really isn't what we want.

So we make the following changes:

{
    "compilerOptions": {
        "target": "ES6", // you can go for higher or lower ECMA versions depending on the node version you intend to target.
        "outDir": "./dist" // to output the compiled files.
    }, "include": [
        "src" // this tells tsc where to read the source files to compile.
    ]
 }

Now let's try out our command:

npm run tsc

You will see errors like:

src/app.ts:21:19 - error TS7006: Parameter 'req' implicitly has an 'any' type.

21 app.use(function (req, res, next) {

That's mean TypeScript works, and it's telling you app.ts -- which is still in Javascript -- has type safety violations, rightfully so. And so we start the conversion.

3. Code conversion and type declarations

First, we need to install type declaration for all modules. Just go with me first, I'll explain what this is all about later. They are named "@types/[modulename]". Whether they are available depends on if the package owner has made it. A lot of them didn't really bothered. In any case, we are only going to do it for node and express as an example, while skipping over type-checking for other modules using // @ts-ignore.

npm install @types/node
npm install @types/express

And convert the app.ts into this:

(Note: Use of @ts-ignore is not recommended, and only for the purposes of this demo.)

// @ts-ignore
import createError = require('http-errors') // change all `var` to import
import express = require('express')
import { join } from 'path' // this is a Node native module. only using #join from `path`
// @ts-ignore
import cookieParser = require('cookie-parser')
// @ts-ignore
import logger = require ('morgan')
// @ts-ignore
import Magic = require('express-routemagic')
const app: express.Application = express() // the correct type declaration style.
// view engine setup
app.set('views', join(__dirname, 'views'))
app.set('view engine', 'hbs')
app.use(logger('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(express.static(join(__dirname, 'public')))
Magic.use(app, { invokerPath: __dirname }) // // need to use `invokerPath` because we are not in api's root dir.
// catch 404 and forward to error handler
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { // type declaration, and changed to use arrow function
    next(createError(404))
})
// error handler
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
    // set locals, only providing error in development
    res.locals.message = err.message
    res.locals.error = req.app.get('env') === 'development' ? err : {}
    // render the error page
    res.status(err.status || 500)
    res.render('error')
})
module.exports = app

What is app.use? It is Express's way of using "middleware" (functions that will run and be given the HTTP request to "read" and do something to it). And it is sequential, and one way is to imagine a HTTP request "falling" through your middlewares: In this case it will first pass through logger('dev'), which logs the request on your terminal, then express.json() which parses the request into json... and so on so forth sequentially.

TypeScript Basics Explanation

The @types/express module you have installed are TypeScript declarations for Express objects. Declarations are like a dictionary -- it explains what something is or isn't.

If you refer lower down in app.ts, the block of // error handler code shows how this "dictionary" is applied to function arguments:

(err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { ... }

What it means is that, the req argument are "extensions" of Express' Request object/prototype (I refuse to use the word "class", because Javascript is irrefutably classless).

So within the function, if you try to use Request as a type that it isn't, or if you try to invoke a method that Request does not have, TypeScript will bitch about it.

(req: express.Request) => {

   req.aMethodThatDoesNotExist() // red curlies underlines, and will not compile.

   if (req === 'someString') {} // TypeScript will tell you this is always false.

})

All that, is in essence a very basic explanation of how TypeScript type-checks your code.

And now if you run npm run tsc again, you should get no errors.

4. Copy all files to "./dist"

TSC will only compile .ts files, rightfully so. But you need to copy the rest of the files over, including those .js files that you either don't intend to convert, or will convert later (that's the beauty, you don't need to always OCD everything to TypeScript -- not all code is worth your time). tsc doesn't seem to provide a good way (see issue here), so we will use the cpy-cli and del-cli modules:

npm install cpy-cli del-cli --save-dev

Set up the npm scripts in package.json.

  1. A prebuild script that uses del shell command (from del-cli module) to delete the old "./dist" folder:
"prebuild": "del './dist'"
  1. A postbuild script that uses cpy shell command (from cpy-cli module) to copy remaining files over:
"postbuild": "cpy --cwd=src '**/*' '!**/*.ts' './../dist' --parents"

// --cwd=src means the Current Working Directory is set to "./src"
// '**/*' means all files and folder in the cwd.
// '!**/*.ts' means excluding all typescript files.
// './../dist' means "basicgram/api/dist", and is relative to "src" folder
// --parents will retain the folder structure in "src"

And your scripts in package.json will be:

{
    "scripts": {
        "start": "node ./dist/bin/www",
        "build": "npm run tsc",
        "prebuild": "del './dist'",
        "postbuild": "cpy '**/*' '!**/*.ts' './../dist' --cwd=src --parents",
        "tsc": "tsc"
    }
}

Now, just to check everything is working, go to "src/routes/index.js" and change title from Express to Express in TypeScript:

res.render('index', { title: 'Express with TypeScript' })

Build and run it:

npm build
npm start

Running express compiled from TypeScript

5. Setting up auto re-compiling

For development, it's inefficient to keep running npm build and npm start. So we are going to use nodemon to auto restart the server on file changes, and ts-node to execute the TypeScript files as though they are Javascript (note: this is intended for development environment and does not output to ./dist):

npm install nodemon ts-node --save-dev

Add the following to package.json:

"scripts": {
    "dev": "nodemon --ext js,ts,json --watch src --exec 'ts-node' ./src/bin/www"
}

Explanation:

--exec: We use --exec flag because nodemon will not use ts-node, instead will use node if the entry file is not ".ts". In this case www is not.
--ext: When --exec is used, we also need to use --ext to manually specify the files to watch for changes.
--watch: This defines which folder nodemon will watch for changes to do a restart.
(Credits to this video)

Run your dev server:

npm run dev

Your API is all fired up! Make some changes to see how nodemon auto re-compiles. See Part 2 to set up your view engine with VueJS in TypeScript.

Endnotes:

  1. Getting Started Using TypeScript with Node.js and Express
  2. Typescript compiler file copy issue
  3. Video: TypeScript Setup With Node & Express

Discussion

markdown guide