personal website
at main 453 lines 20 kB view raw view rendered
1--- 2title: "Back to Basics: Making a Node.js Web Application" 3description: Taking a break from Javascript (meta) frameworks and making a web application and website with Hono and Node.js as the foundation. 4date: "2023-10-28" 5draft: false 6link: https://github.com/zeucapua/robin-tutorial 7--- 8 9### Why? 10 11For my latest project, I wanted to get away from the bustling modern world of JS (meta) frameworks and return to the basics. Since I just started learning web development over a year ago, I’ve only been learning abstractions based on any given UI framework. But I wanted to know if there is a simpler way to understand and make small web applications? Here are my notes on how to make a small web application from start to finish! 12 13### What are we building? 14 15Robin is a project time tracker, inspired by [Watson CLI](https://github.com/TailorDev/Watson) tool. A user can create projects and simply clock in and clock out of a session. All sessions are counted to get a total time spent doing projects. The front end will allow for simple CRUD actions to manage the data. 16 17### The Stack 18 19Robin will be a Node.js (Node) web application, built with [Hono](http://hono.dev) as our server framework. Deployed on [Railway](http://railway.app) alongside a PostgreSQL database. The database is managed and query using [Drizzle ORM](http://orm.drizzle.team). We will be setting up the project so that we can create a front-end website using `tsx` components with [HTMX](http://htmx.org) for a future follow-up blog. 20 21If you want to see the codebase, check out the annotated [Github repository](https://github.com/zeucapua/robin-tutorial) and give it a star if you found it useful! 22 23### How does it work? 24 25![shapes at 23-10-30 16.46.52.png](/Back%20to%20Basics%20Making%20a%20Node%20js%20Web%20Application%209447f567860d464283ee35f0bda5f2d2/shapes_at_23-10-30_16.46.52.png) 26 27Before writing any code, I think we should take a step back and check how websites work. When someone goes to a URL, the browser makes an HTTP GET request to the index endpoint. Endpoints are how clients, like our browser, can interact and tell the server to do things. In this case, the server starts turning the TSX template we wrote into HTML and returns it back with any Javascript to the browser. The browser then takes the HTML and JS to render the page so the user can look and interact with it. 28 29To put it in other words, we deal with a client making an HTTP request to the server that responds back with data we can parse and use. We can put all of our pages and CRUD actions into server endpoints that we can interact with. 30 31### Author’s Note 32 33This blog assumes **NOTHING** of the reader. That means that this blog will have sections setting up the project in *painfully detailed step-by-step instructions.* However, I will not be going over installing terminal commands like `npm/pnpm` , `tsc` , `git`, `gh`, etc. I will try my darnedest not to be sidetracked, and keep my focus on creating and deploying a Node.js web application, but no promises. 34 35### Installation 36 37Here’s how to get started (using a terminal): 38 39- Create a new folder (`mkdir robin-tutorial` ) and go inside it (`cd robin-tutorial`) 40- We’ll start a new Node project by using `pnpm init`, which should generate a `package.json` file. 41 - For this tutorial, we will be using `pnpm` , but `npm` should be similar (`pnpm init` = `npm init` , `pnpm add` = `npm install` , etc.) 42- From here we have to install our packages, which in our case are: 43 - Hono (our server framework): `pnpm add hono @hono/node-server` 44 - Dotenv (to access our `.env` variables): `pnpm add dotenv` 45 - Drizzle ORM (to manipulate our database): `pnpm add drizzle-orm pg` & `pnpm add -D drizzle-kit @types/pg` 46 - TSX (our HTML templates in TS): `pnpm add -D tsx` 47- Before moving on, you can look inside the folder to ensure that we have a `node_modules` folder, `package.json` file (which we change in a moment), and a `pnpm-lock.yaml` file (I assume this sets the packages’ version). 48- To setup TSX, run `tsc --init` to create a `tsconfig.json` that we will edit to ensure the following properties are not commented. Use a text editor to recreate the following: 49 50```json 51{ 52 "compilerOptions": { 53 "target": "es2016", 54 "jsx": "react-jsx", 55 // some stuff... 56 "jsxImportSource": "hono/jsx", 57 58 // some stuff... 59 // the following are already set by `tsc --init`, but make sure anyway! 60 "module": "commonjs", 61 "esModuleInterop": true, 62 "forceConsistentCasingInFileNames": true, 63 "strict": true, 64 "skipLibCheck": true 65 } 66} 67``` 68 69- Afterwards, let’s add a new `src` folder with our files inside: `index.tsx` (our app’s entry point), `components.tsx` (our JSX templates), and `schema.ts` (used to model our database with Drizzle). 70- Lastly, let’s modify our `package.json` and change our main file and add scripts to run our application, including some for using Drizzle (will be explained later): 71 72```json 73{ 74 // ... 75 "main": "src/index.tsx", 76 "scripts": { 77 "start": "tsx src/index.tsx", 78 79 // for drizzle, will be used later 80 "generate": "drizzle-kit generate:pg", 81 "migrate": "drizzle-kit migrate:pg" 82 }, 83 // ... 84} 85``` 86 87### Did you know Hono means ‘Fire’ in Japanese? 88 89Hono is a Node server framework which makes coding endpoints easy. Other similar frameworks would be Elysia, Fastify, and Express. 90 91To start our project, start by creating a new `Hono` object and subsequently call functions with the appropriate HTTP request and endpoint. Afterwards export and serve the web app. This will be inside our `index.tsx` file. 92 93```tsx 94// index.tsx 95// --------------------------------------- 96 97/* 🪂 Import packages (installed via npm/pnpm) */ 98 99// Hono packages 100import { Hono } from 'hono'; 101import { serve } from "@hono/node-server"; 102 103// loads environment variables from `.env`, will be used later 104import * as dotenv from "dotenv"; 105dotenv.config(); 106 107// --------------------------------------- 108 109/* 🏗️ Configure Hono Web Application */ 110 111// initialize web application 112const app = new Hono(); 113 114// --------------------------------------- 115 116/* 🛣️ Route Endpoints */ 117 118// GET index page 119app.get("/", async (c) => { 120 // return HTML response 121 return c.html( 122 <h1>Hello world!</h1> 123 ); 124}); 125 126export default app; 127 128// --------------------------------------- 129 130/* 🚀 Deployment */ 131 132// use `.env` set PORT, for Railway deployment 133const PORT = Number(process.env.PORT) || 3000; 134 135// become a server, to deploy as Node.js app on Railway 136serve({ 137 fetch: app.fetch, 138 port: PORT 139}); 140 141// --------------------------------------- 142``` 143 144Now going back to the terminal, we can run our web application by using the start script from the `package.json` file that we set up earlier: `pnpm run start`. Use the browser and go to `[http://localhost:3000](http://localhost:3000)` and you should be greeted with a big bold “**Hello world!”** 145 146<aside> 147💡 If you’re familiar with modern JS (meta) frameworks, making any changes while a development (dev) server is running will cause a re-render, allowing you to see changes in styling, for example. This is because of HMR (Hot Module Reloading). We **don’t** have HMR in this project. So any further changes will require you to stop (`ctrl-c` in the terminal) and restart the dev server (`pnpm run start`). 148 149</aside> 150 151### Database Setup with Drizzle (fo’ shizzle) 152 153Now that we have the basic web application setup, let’s move our focus onto the database that we’ll use for our time tracking functions. Drizzle ORM (Object-Relational Mapping) is a library to manage and communicate with the database via Typescript (TS) code. We can use the ORM to create the source of truth for the database’s schema. Let’s set it (and our hosted DB) up! 154 155- Provision a new PostgreSQL (postgres) database on Railway by creating a new project. 156 157![Untitled](/Back%20to%20Basics%20Making%20a%20Node%20js%20Web%20Application%209447f567860d464283ee35f0bda5f2d2/Untitled.png) 158 159- Once deployed, go to the **Variables** tab on the postgres service and copy the `DATABASE_URL` value… 160 161![Untitled](/Back%20to%20Basics%20Making%20a%20Node%20js%20Web%20Application%209447f567860d464283ee35f0bda5f2d2/Untitled%201.png) 162 163- …which we will add to a new `.env` file in our root directory. 164 165``` 166# .env 167DATABASE_URL=postgresql://<username>:<password>@<location>:<port>/<dbname> 168``` 169 170- Moving on, we now need to define the shape of our data in our `schema.ts` file using Drizzle: 171 172```tsx 173// schema.ts 174// --------------------------------------- 175 176/* Import packages (installed via npm/pnpm) */ 177// drizzle-orm packages 178import { relations } from "drizzle-orm"; 179import { pgTable, serial, timestamp, varchar } from "drizzle-orm/pg-core"; 180 181// --------------------------------------- 182 183/* Data Models */ 184// >> find more information on defining the schema: 185// >> https://orm.drizzle.team/docs/sql-schema-declaration 186export const projects = pgTable("projects", { 187 id: serial("id").primaryKey(), 188 name: varchar("name", { length: 100 }).unique() 189}); 190 191export const sessions = pgTable("sessions", { 192 id: serial("id").primaryKey(), 193 start: timestamp("start").defaultNow(), 194 end: timestamp("end"), 195 projectName: varchar("project_name").notNull() 196}); 197 198/* Relationships Between Models */ 199// find more information on declaring relations: 200// https://orm.drizzle.team/docs/rqb#declaring-relations 201export const projects_relations = relations(projects, ({ many }) => ({ 202 sessions: many(sessions) 203})); 204 205export const sessions_relations = relations(sessions, ({ one }) => ({ 206 project: one(projects, { 207 fields: [sessions.projectName], 208 references: [projects.name] 209 }) 210})); 211 212// --------------------------------------- 213``` 214 215This schema will create a one-to-many relationship where a **project** can have multiple **sessions**. Visually it’ll look like so, thanks to [DiagramGPT](https://www.eraser.io/diagramgpt): 216 217![diagram-export-10-28-2023-3_06_37-AM.png](/Back%20to%20Basics%20Making%20a%20Node%20js%20Web%20Application%209447f567860d464283ee35f0bda5f2d2/diagram-export-10-28-2023-3_06_37-AM.png) 218 219- To turn this schema into our database’s tables, we need to create a `drizzle.config.ts` file in the root directory to setup the migration correctly, giving it the schema file, the folder that will hold the migrations, and the `DATABASE_URL` as the connection string to the database. 220 221```tsx 222// --------------------------------------- 223 224/* Import packages (installed via npm/pnpm) */ 225 226// to type check the configuration 227import type { Config } from "drizzle-kit"; 228 229// load .env variables 230import * as dotenv from "dotenv"; 231dotenv.config(); 232 233// --------------------------------------- 234 235/* declare Drizzle config */ 236export default { 237 schema: "./src/schema.ts", 238 out: "./drizzle", 239 driver: "pg", 240 dbCredentials: { 241 connectionString: process.env.DATABASE_URL as string 242 } 243} satisfies Config 244 245// --------------------------------------- 246``` 247 248- Once that is set, we need to generate a SQL migration file using the `generate` script we made earlier inside the `package.json` file, then push the changes with the `migrate` script. 249 250``` 251# scripts declared in 'package.json' 252 253# runs 'drizzle-kit generate:pg' 254pnpm run generate 255 256# runs 'drizzle-kit push:pg' 257pnpm run migrate 258``` 259 260- Check your Railway deployment to see if the migration went through by ensuring our **projects** and **sessions** tables are in the postgres’ data tab. 261 262![Untitled](/Back%20to%20Basics%20Making%20a%20Node%20js%20Web%20Application%209447f567860d464283ee35f0bda5f2d2/Untitled%202.png) 263 264- Finally, import the relevant packages and setup the Drizzle client ready for use in the next 265 266```tsx 267// index.tsx 268// --------------------------------------- 269 270/* 🪂 Import packages (installed via npm/pnpm) */ 271// ... 272 273// Database Driver 274import { Pool } from "pg"; 275 276// Drizzle ORM packages 277import * as schema from "./schema"; 278import { desc, eq } from "drizzle-orm"; 279import { drizzle } from "drizzle-orm/node-postgres"; 280 281// --------------------------------------- 282 283/* 🏗️ Configure Hono Web Application */ 284// ... 285 286// create pool connection to database 287const pool = new Pool({ 288 connectionString: process.env.DATABASE_URL 289}); 290 291// initialize ORM client with schema types 292const database = drizzle(pool, { schema }); 293 294// --------------------------------------- 295``` 296 297### Implementing CRUD API with HTML Endpoints 298 299Let’s implement the `GET` and `POST` HTTP endpoints to create and read **projects** to demonstrate how it’s written in Hono. Endpoints are made by calling the HTTP verbs’ function on the `app` variable, passing a string representing the path and an async function with the context as a parameter. Here, the context (`c`) is used to handle both the incoming `Request` and outgoing `Response`. 300 301```tsx 302// index.tsx 303// --------------------------------------- 304 305/* 🛣️ Route Endpoints */ 306// ... 307 308// GET: return project by name 309app.get("/api/project/:name", async (c) => { 310 // get route parameter (denoted with ':') 311 const name = c.req.param("name") as string; 312 313 // query database to find project with name 314 const result = await database.query.projects.findFirst({ 315 where: eq(schema.projects.name, name) 316 }); 317 318 // return JSON response 319 return c.json({ result }); 320}); 321 322// POST: create new project with name 323app.post("/api/project/:name", async (c) => { 324 // get route parameter (denoted with ':') 325 const name = c.req.param("name") as string; 326 327 // create a new project 328 const result = await database 329 .insert(schema.projects) 330 .values({ name }) 331 .returning(); 332 333 // return JSON response 334 return c.json({ result: result[0] }); 335}); 336``` 337 338For this code snippet, the endpoints will run database queries and inserts with our Drizzle client based on the name given as part of the path and then return the results. We separate these functions with different HTTP verbs, even if they are under the same path/endpoint. 339 340Now what are projects but holders of our sessions. Implementing these aren’t going to be as easy as our project endpoints since we need to ensure that all sessions started must end, as well as ensuring we are returning null if there is no latest session for the project. 341 342```tsx 343// index.tsx 344// --------------------------------------- 345 346/* 🛣️ Route Endpoints */ 347// ... 348 349// GET latest session under project name 350app.get("/api/session/:name", async (c) => { 351 const name = c.req.param("name") as string; 352 353 // get latest session 354 const latest = await database.query.sessions.findFirst({ 355 where: eq(schema.sessions.projectName, name), 356 orderBy: [desc(schema.sessions.start)] 357 }); 358 359 // return null if latest is undefined 360 return c.json({ result: latest ?? null }); 361}); 362 363// POST create a new session under project name 364app.post("/api/session/:name", async (c) => { 365 const name = c.req.param("name") as string; 366 367 // get latest session 368 const latest = await database.query.sessions.findFirst({ 369 where: eq(schema.sessions.projectName, name), 370 orderBy: [desc(schema.sessions.start)] 371 }); 372 373 // if no session OR latest already has an end time, then create a new session 374 // else end the current session 375 if (!latest || latest.end !== null) { 376 const result = await database 377 .insert(schema.sessions) 378 .values({ projectName: name }) 379 .returning(); 380 381 return c.json({ result: result[0] }); 382 } 383 else { 384 const updated = await database 385 .update(schema.sessions) 386 .set({ end: new Date }) 387 .where( eq(schema.sessions.id, latest.id) ) 388 .returning(); 389 390 return c.json({ result: updated[0] }); 391 } 392}); 393``` 394 395Now we can test our application by running a local development (dev) server with `pnpm run start` in a terminal, and then using another to make `curl` requests. The following will make `POST` requests to create a project and session, `GET` the current session, and lastly `POST` to end the latest session. These should give you back JSON responses like those below on each request. 396 397```bash 398> curl -X POST http://localhost:3000/api/project/coding 399{"result":{"id":1,"name":"coding"}} 400 401> curl -X POST http://localhost:3000/api/session/coding 402{"result":{"id":2,"start":"2023-10-29T22:43:25.588Z","end":null,"projectName":"coding"}} 403 404> curl -X POST http://localhost:3000/api/session/coding 405{"result":{"id":2,"start":"2023-10-29T22:43:25.588Z","end":"2023-10-29T22:44:17.350Z","projectName":"coding"}}% 406``` 407 408### Git & Github Repository Setup 409 410We can easily deploy this application by putting this project in a repository on Github and then hosting it in our Railway project alongside our postgres database. Here’s the step by step (according to Notion AI): 411 4121. Create a new repository on GitHub. 4132. In your terminal, navigate to the root directory of your project. 4143. Initialize Git in the project folder by running the command: `git init`. 4154. Add all the files in your project to the Git repository by running the command: `git add .`. 4165. Commit the changes by running the command: `git commit -m "Initial commit"`. 4176. Add the remote repository URL as the origin by running the command: `git remote add origin <remote_repository_url>`. 4187. Push the changes to the remote repository by running the command: `git push -u origin master`. 4198. Provide your GitHub username and password when prompted. 420 421After following these steps, your project will be pushed to GitHub and will be visible in your repository. 422 423### Deploying the Node.js Web Application on Railway 424 425From here, go back to the Railway project and press ‘Add’. Choose ‘Deploy from Github’ and find your repository. It should start deploying right away, **but** we need to change a few settings to get it working properly. 426 427To connect to our website publicly, we want to go to service’s ‘Settings’, go down to ‘Networking’ and press the ‘Generate Domain’ button. This should give you a URL you can enter with your browser. 428 429![2023-10-30_14-49.png](/Back%20to%20Basics%20Making%20a%20Node%20js%20Web%20Application%209447f567860d464283ee35f0bda5f2d2/2023-10-30_14-49.png) 430 431We also need to give the website access to our postgres database. Before we added the `DATABASE_URL` to a `.env` file, but since that isn’t in our repository (because it can be leaked on Github), Railway makes this easy for us by going to the ‘Variables’ tab and adding a ‘Variable Reference’, where we can add our `DATABASE_URL` variable from the database automatically. 432 433![Untitled](/Back%20to%20Basics%20Making%20a%20Node%20js%20Web%20Application%209447f567860d464283ee35f0bda5f2d2/Untitled%203.png) 434 435And now the project is live online! No need to run a local server, you can now access your endpoint as long as you have internet connection. For example, you can run the same `curl` requests, but now with the live URL (**note**: use `https` , not `http` when using the live URL). 436 437```bash 438> curl -X POST https://robin-tutorial-production.up.railway.app/api/project/coding 439{"result":{"id":1,"name":"coding"}} 440``` 441 442### That’s It…. FOR NOW 443 444We now have a working CRUD web application online! Next steps is to get the TSX setup to use with a new blog on how to use HTMX. This will turn our application to an actual, honest to goodness, functional **website,** like with inputs, buttons, and styling! I’m working hard behind the scenes to learn how to implement HTMX and keep it understandable for you and me 😅 445 446That’s in the future though! For now, I’d like to thank you for reading this blog. I very much appreciate it, and if you can do me a favor, take a look at the links down below. Catch you in the next one! 447 448### Shameless Plugs 449 450- If you’d like to clone the source code for this project, it is public with a commented repository on my Github [here](https://github.com/zeucapua/robin-tutorial). 451- This project was made live on my Twitch stream. Code new projects with me weekly on [twitch.tv/zeu_dev](http://twitch.tv/zeu_dev). 452- Any comments or questions can reach me on Twitter. Follow me at [twitter.com/zeu_dev](http://twitter.com/zeu_dev). 453- Interested on other stuff? Visit my personal website at [zeu.dev](http://zeu.dev) and my other blogs on [thoughts.zeu.dev](http://thoughts.zeu.dev)!