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:
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)
- Setting up Node and Express API with TypeScript
- Setting up VueJs with TypeScript
- Setting up Postgres with Sequelize ORM
- Basic Vue templating and interaction with API
- 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, when the time comes, you are likely able to switch to Deno and reuse much of your API codebase if you generally follow best practices in this tutorial. You will be able to re-use all your view coding as it is not coupled to the API.
To be completely honest, Instagram can't be built in a single tutorial, so admittedly the title of this post is an exaggeration. 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.
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 so you won't need to learn new templating syntax. But you hardly will use since we will only be using Express for API service -- but it's there for you when you need it.
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 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 the routing requires:
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 (there are many useful and time-proven ones, but didn't see the need to port themselves over to TypeScript syntax). Throughout this tutorial, figuring how to convert some JS syntax to TypeScript were either painful or close to impossible. But well, we soldier on.
Since now we need to compile our TS to JS for Node runtime, we will need a few steps to get there.
1. Pack your Express into a "src" folder like this:
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: I think it is OK to have mixed codebase. Not everything needs to be in TypeScript, plus you will be missing out a lot of really good JS modules. But I'm sure this statement invokes agitation amongst TypeScriptors. Oh well, make your case below.
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:
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).
It will search compile all
.ts
files inside ofbasicgram/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 throughlogger('dev')
, which logs the request on your terminal, thenexpress.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 "of the same type and -- for a lack of better word -- form" with 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
.
- A
prebuild
script that usesdel
shell command (fromdel-cli
module) to delete the old "./dist" folder:
"prebuild": "del './dist'"
- A
postbuild
script that usescpy
shell command (fromcpy-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
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.
Top comments (5)
Wow - just in time! I wanted to tackle a bigger project with vue + typescript. I haven't used Express since i learned it a few years ago, so excited to work on that. And funny you mentioned Deno - that was on my to learn list too! Had it been ready, this project would have been the trifecta of every single thing on my to learn list!
Will start this tonight and excited to share any thoughts! You rock!
Hey there, thanks for the kind words. I was thinking I put together all that I have learnt from other people's tutorials, and give back. And in a way, it was just all a bunch of notes I made as I was coding stuff put together.
I do have to write my own libraries to solve problems that I can't find libraries to solve. But you are welcomed to criticise those or offer alternatives. Cheers mate, chat me up and I will be happy to help.
Oh btw, if you learn Node+express, you are learning parts of Deno. There's already a few Express-lookalike Deno modules. So don't worry about it!
Hey @calvintwr - I wanted to give you an update. I've been tackling this bit by bit. I reached step 3 and the whole Postgres bit got me stuck. The Postgres setup is tripping me up. I'm going through a quick course on it before I continue.
Either way - I've been saving feedback for little things I noticed. But as of the middle of step 3, I've been having a lot of fun in this new world! Really huge thanks for the write-up and I'll continue pushing in the next few days.
Hey there! What about Postgres that’s getting you stuck? Maybe I can help.