For this tutorial, I am assuming you are running Linux! Mac should work fine too, though you might have to go looking for the MacOS version of some tools. If you're on Windows, either (1) install WSL or (2) be prepared to find your own alternatves.
First, we'll install Node.JS and the Node package manager. NodeJS is a way for you to run Javascript for backend applications, rather than just inside the browser. NPM (Node Package Manager) comes with Node and is what we use to install packages (extral tools and libraries) for our node application.
# First, install NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
# Refresh your bash to activate it (or restart your terminal)
source ~/.bashrc
# Install and activate LTS version of Node
nvm install --lts
nvm use --lts
These are the steps for just getting the repository running.
Enter the frontend folder, install the NPM dependencies, and you're ready to launch!
cd todo-list-frontend
npm install
npm run dev
It's the same as the frontend, but the launch command is different.
cd todo-list-backend
npm install
npm run start
These are the steps for creating this repository from scratch!
First, create a new directory, then enter it:
mkdir todo-list-backend
cd todo-list-backend
NPM sets up your application by storing all its configurations in the folder you build you app in. The most important files managed by NPM are the package.json
and package.lock.json
files, which track what packages you have installed.
In the folder you created, initialise your Node app:
npm init -y
Now, install the Express and TypeScript NPM packages:
npm install express
npm install -D typescript @types/express @types/node
TypeScript adds a number of extensions and greatly changes parts of the process to building your application. Initialise these parts of your project like this:
npx tsc --init
This will generate a tsconfig.json
file, which contains your configuration for Typescript. Replace the (very long) content of the file with the following:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
As one last piece of setup, we'll add the following lines to your package.json
to allow us to run the backend using NPM:
"scripts": {
"start": "ts-node src/index.ts",
"build": "tsc",
"serve": "node dist/index.js"
},
Make sure to replace the original "scripts"
... if it's there!
Your final package.json
should look something like this:
{
"name": "todo-list-backend",
"version": "1.0.0",
"description": "todo list backend in express",
"main": "index.js",
"scripts": {
"start": "ts-node src/index.ts",
"build": "tsc",
"serve": "node dist/index.js"
},
"author": "me",
"license": "ISC",
"dependencies": {
"express": "^4.18.3"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.11.24",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}
Now, we can write our TypeScript code!
Inside your backend folder, create a /src/
folder, and inside it a file named index.ts
.
In it, add the following code:
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
app.get('/', (req: Request, res: Response) => {
res.send('Hello, TypeScript Express!');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
There's a lot going on, but lets take it bit by bit:
- First, we import the express.js components we'll need using node.js' import syntax.
- We then instantiate our express server with
express()
- the server is just an object! We store it in theapp
variable.- We also store our port as a variable, just to avoid making magic numbers.
- Next, I'll skip ahead to explain something. The server is manipulated by calling methods on the
app
variable.app.listen()
launches the server with the given configuration.- This method takes in two things: a port, and a callback - a function to call when the server is properly set-up. Passing functions as variables like this is something you'll see very often in JavaScript/TypeScript. The special syntax used here is called an arrow function, and is used to quickly define a function without the full
function() { }
syntax. Arrow functions work like this:
Note that you dont specify return types when creating an arrow function. Just return the thing in the body!(parameter1, parameter2) => { // function body code }
- This method takes in two things: a port, and a callback - a function to call when the server is properly set-up. Passing functions as variables like this is something you'll see very often in JavaScript/TypeScript. The special syntax used here is called an arrow function, and is used to quickly define a function without the full
- Now that we understand arrow functions, let's go back - the
app.get()
function is used to define an endpoint on our server.- We call
app.get()
to specify aGET
request endpoint. To specify aPOST
endpoint instead, you would doapp.post()
, for example. app.get()
takes 2 parameters:- First, we take the endpoint. This is the path on the server's URL that should be called to access this endpoint. In our case, it's
/
, meaning the base of our server (localhost:3000/
). - The second is the handler - a function specifying what the server should do with requests to this endpoint. The handler function must take in two parameters: a
Request
object and aResponse
object. These let you interact with the HTTP request sent to the server and the HTTP response you will send respectively.- In our
app.get()
, we ignore thereq
Request
for now, and only work with a the response object.re.send()
sends the final response ot the request back to whomever made the request. In our case, we just say hello!
- In our
- First, we take the endpoint. This is the path on the server's URL that should be called to access this endpoint. In our case, it's
- We call
With that all set up, you should now be able to build and run your app:
npm run build
npm run serve
# Alternatively, just do:
npm run start
These are using the shortcuts we set up in our package.json
earlier. Here's what they do:
npm run build
just runstsc
, the typescript compiler. This will compile our TypeScript into JavaScript, and place it in thedist/
folder.npm run serve
takes the compiled JavaScript and executes this- Alternatively,
npm run start
is a shortcut to do both of these quickly, useful during development.
After doing the above in the terminal, you should see some output like this:
> todo-list-backend@1.0.0 serve
> node dist/index.js
Server running at http://localhost:3000
This means the server is now running on our local machine, at port 3000. If we navigate to http://localhost:3000
, the root of our API, we should be able to see the reply we specified!
Now, lets extend our application a bit. Replace the code in index.ts
with the following:
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
// State (count the number of requests)
let count = 0;
// When someone makes a request to http://localhost:3000/, run this code, and give this reply
app.get('/', (req: Request, res: Response) => {
res
.setHeader('Access-Control-Allow-Origin', '*')
.status(200)
.send('Hello, TypeScript Express!');
});
app.get("/ping", (req: Request, res: Response) => {
count++;
let responseContent = {
message: "pong",
count: count,
};
res
.setHeader('Access-Control-Allow-Origin', '*')
.status(200)
.json(responseContent);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
We've done 3 main things here.
- First, we added the
.setHeader()
and.status()
parts to our responses. The first deals with CORS (don't worry about this for now) and the second sets the status code of our reply to 200 (meaning OK) explicitly. Status codes are a way for the server to quickly tell the client how a HTTP request went - for example, 404 means the thing requested could not be found. - Next, we added another
app.get()
route, at/ping
. This means wheneverhttp://localhost:3000/ping
is accessed, this code will be run to produce a response.- Instead of calling
.send()
in this handler, we call.json()
, which lets us send our reponse as a JSON object, standard for APIs.
- Instead of calling
- We also added a variable
count
at the base of our program. Inside the/ping
handler, we return the value of this count in the JSON. We also increment it during the handler, so every time a request to the endpoint is made, the count value will increase by 1!
Try running the server again (if the old one is still running, exit it by pressing ctrl+C in the terminal). If you go to http://localhost:3000/ping
, you should see a JSON response. Every time you refresh, you should see the count
field in the response increase!
And that's it! Your simple backend is done.
Svelte itself is the tool we can use to write frontend web pages. For a fully-fledged web application that includes a server and many extra features, we use SvelteKit, fully-fledged webapp development framework for Svelte.
We can create a new sveltekit project easily using NPM from the terminal like this:
npm create svelte@latest todo-list-frontend
Running this command will give you an interactive setup tool. Choose the following options:
- Which Svelte app template? - Skeleton project
- Add type checking with TypeScript? - Yes, using TypeScript syntax
- Select additional options (use arrow keys/space bar) - Check (with space):
- Add ESLint for code linting
- Add Prettier for code formatting
If you mess anything up, just delete all the files created in your local folder.
This will create a todolist-frontend
(or whatever you named it) folder with the basis of your app.
From here, navigate into the app directory, install the dependencies (it only defines them when you run the previous step):
cd todolist-frontend
npm install
Once this is done, you can run your app in test mode:
npm run dev
Running this will host your webapp on your PC, probably at http://localhost:5173/
. Navigate there to see it in action! You should see a big "Welcome to SvelteKit" header and some body text.
Now we'll write the "ping" part of our "ping-pong" app on this page.
Open up your favourite editor (VSCode if you don't have one) and open the todolist-frontend
folder. Now, open up the src/routes/+page.svelte
file. It should look like this:
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
In SvelteKit, the src/routes
folder defines pages in your website. For example, the code defining the page at www.yoursite.com/beans
would be stored in the file src/routes/beans/+page.svelte
. Since the src/routes/+page.svelte
file is in the routes
folder itself, it defines the page we see when we navigate to the base of the website.
First, we will add some code to the frontend that will contact the backend. Add the following to the top of your src/routes/+page.svelte
file:
<script lang="ts">
let pingResponseMessage: string;
let pingResponseCount: number;
async function ping() {
// Make a GET request to the ping-pong backend API
let response = await fetch("http://localhost:3000/ping");
// Get the JSON body from the request
let responseJson = await response.json();
// Set our pingResponse variable to change the UI
pingResponseMessage = responseJson.message;
pingResponseCount = responseJson.count;
}
</script>
There's a log going on here, but lets go through it step by step.
- The
<script lang="ts">
tag at the beginning denotes that we are writing TypeScript code. It is Svelte convention to put all your JavaScript/TypeScript in a<script>
tag at the top. - First,
let pingResponseMessage: string
creates a variable calledpingResponseMessage
, which we use to store the result of contacting the backend. The default value of the variable isundefined
. All the same goes forpingResponseCount
- Then, the
async function ping()
and following lines create a function calledping()
. First, this function usesfetch
to make a GET request to the backend, then takes the JSON content of the backend response, and sets thepingResponseMessage
variable to themessage
value in the response.
Basically, we make a request to the backend, and store the reply!
Now that we have our backend request, lets make a frontend to show it running. Replace the existing HTML in src/routes/+page.svelte
with the following:
<h1>POGGERS Ping Pong</h1>
<button on:click={ping}>Do Ping</button>
{#if pingResponseMessage != undefined}
<p>The ping response message was: {pingResponseMessage}</p>
<p>The ping response count was: {pingResponseCount}</p>
{/if}
Let's break this down too:
<h1>POGGERS Ping Pong</h1>
is just basic HTML, and shows a relevant title.<button on:click={ping}>Do Ping</button>
creates a button on the web page. The button will say "Do Ping".- We give the button a special parameter,
on:click={ping}
. As a rule of thumb, anything with curly braces ('{
', '}
') is Svelte-specific stuff. Here, we are saying: "When the button is clicked, run theping
function defined at the top". - Finally, the longest bit:
{#if pingResponseMessage != undefined}
is again Svelte-specific. The{#if <condition>}
and{/if}
statements mean that any HTML between them (in this case two lines) will only be shown if the condition is true. In this case, if thepingResponseMessage
gets a different value than its initial one.- The next two
<p>
lines then simply display the values ofpingResponseMessage
andpingResponseCount
.
All together, your src/routes/+page.svelte
should look like this:
<script lang="ts">
let pingResponseMessage: string;
let pingResponseCount: number;
async function ping() {
// Make a GET request to the ping-pong backend API
let response = await fetch("http://localhost:3000/ping");
// Get the JSON body from the request
let responseJson = await response.json();
// Set our pingResponse variable to change the UI
pingResponseMessage = responseJson.message;
pingResponseCount = responseJson.count;
}
</script>
<h1>POGGERS Ping Pong</h1>
<button on:click={ping}>Do Ping</button>
{#if pingResponseMessage != undefined}
<p>The ping response message was: {pingResponseMessage}</p>
<p>The ping response count was: {pingResponseCount}</p>
{/if}
And that's it! If your backend is running on https://localhost:8081
, you should not be able to ping the backend and see the response.