From 40d9480edde39facd5bf013bd1a4ea09077aeebd Mon Sep 17 00:00:00 2001 From: Simon Haines Date: Mon, 15 Jul 2024 10:55:33 +0100 Subject: [PATCH 01/13] Update web-socket-server.md typo --- src/express/web-socket-server.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/express/web-socket-server.md b/src/express/web-socket-server.md index dc71041..a252ff5 100644 --- a/src/express/web-socket-server.md +++ b/src/express/web-socket-server.md @@ -43,7 +43,7 @@ server.on('connection', (socket) => { }) ``` -When the `'connection'` even is emitted, we are passed a web socket, which we +When the `'connection'` event is emitted, we are passed a web socket, which we can call whatever we want (here, we used `socket`). ## Listening for socket events From bf05c07b9a366742782fc2a2fac69837dac6d2b9 Mon Sep 17 00:00:00 2001 From: Simon Haines Date: Mon, 15 Jul 2024 11:11:20 +0100 Subject: [PATCH 02/13] Update syncing-clients.md badly named variable --- src/express/syncing-clients.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/express/syncing-clients.md b/src/express/syncing-clients.md index e6307d8..29ebd6a 100644 --- a/src/express/syncing-clients.md +++ b/src/express/syncing-clients.md @@ -73,8 +73,8 @@ server.on('connection', socket => { // when we receive a message // forward the message to all clients socket.on('message', message => { - for (let socket of sockets) { - socket.send(message) + for (let recipient of sockets) { + recipient.send(message) } }) }) From 437c5081060b947201267130d53691cd8e3f7ed0 Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Mon, 15 Jul 2024 16:22:45 +0000 Subject: [PATCH 03/13] Add javalin stuff --- .vitepress/config.mjs | 1 + .vitepress/sidebars/index.js | 1 + .vitepress/sidebars/javalin.js | 50 +++++++ .vitepress/sidebars/topnav.js | 9 ++ src/javalin/adding-a-model-layer.md | 64 +++++++++ src/javalin/adding-css.md | 0 src/javalin/auth.md | 0 src/javalin/body-and-headers.md | 60 +++++++++ src/javalin/connecting-to-a-database.md | 47 +++++++ src/javalin/creating-a-server.md | 108 +++++++++++++++ src/javalin/css-and-assets.md | 35 +++++ src/javalin/index.md | 7 + src/javalin/managing-the-database.md | 80 +++++++++++ src/javalin/middleware.md | 1 + src/javalin/model-layer.md | 1 + src/javalin/partial-templates.md | 0 src/javalin/query-params.md | 62 +++++++++ src/javalin/request-response.md | 115 ++++++++++++++++ src/javalin/routing.md | 112 ++++++++++++++++ src/javalin/schema-validation.md | 46 +++++++ src/javalin/sending-errors.md | 111 ++++++++++++++++ src/javalin/static-files.md | 49 +++++++ src/javalin/template-partials.md | 168 ++++++++++++++++++++++++ src/javalin/url-params.md | 64 +++++++++ src/javalin/user-input.md | 78 +++++++++++ src/javalin/using-loops.md | 55 ++++++++ src/javalin/views-and-static.md | 85 ++++++++++++ src/javalin/views-and-templates.md | 55 ++++++++ 28 files changed, 1464 insertions(+) create mode 100644 .vitepress/sidebars/javalin.js create mode 100644 src/javalin/adding-a-model-layer.md create mode 100644 src/javalin/adding-css.md create mode 100644 src/javalin/auth.md create mode 100644 src/javalin/body-and-headers.md create mode 100644 src/javalin/connecting-to-a-database.md create mode 100644 src/javalin/creating-a-server.md create mode 100644 src/javalin/css-and-assets.md create mode 100644 src/javalin/index.md create mode 100644 src/javalin/managing-the-database.md create mode 100644 src/javalin/middleware.md create mode 100644 src/javalin/model-layer.md create mode 100644 src/javalin/partial-templates.md create mode 100644 src/javalin/query-params.md create mode 100644 src/javalin/request-response.md create mode 100644 src/javalin/routing.md create mode 100644 src/javalin/schema-validation.md create mode 100644 src/javalin/sending-errors.md create mode 100644 src/javalin/static-files.md create mode 100644 src/javalin/template-partials.md create mode 100644 src/javalin/url-params.md create mode 100644 src/javalin/user-input.md create mode 100644 src/javalin/using-loops.md create mode 100644 src/javalin/views-and-static.md create mode 100644 src/javalin/views-and-templates.md diff --git a/.vitepress/config.mjs b/.vitepress/config.mjs index d27ad2d..01b8dad 100644 --- a/.vitepress/config.mjs +++ b/.vitepress/config.mjs @@ -23,6 +23,7 @@ export default defineConfig({ '/bash/': sidebars.bash, '/vscode/': sidebars.vscode, '/express/': sidebars.express, + '/javalin/': sidebars.javalin, '/html-css/': sidebars.htmlCss, '/js/': sidebars.javascript, '/java/': sidebars.java, diff --git a/.vitepress/sidebars/index.js b/.vitepress/sidebars/index.js index fa5ac72..a31c80a 100644 --- a/.vitepress/sidebars/index.js +++ b/.vitepress/sidebars/index.js @@ -1,6 +1,7 @@ export { bash } from './bash.js' export { vscode } from './vscode.js' export { express } from './express.js' +export { javalin } from './javalin.js' export { htmlCss } from './html-css.js' export { javascript } from './javascript.js' export { java } from './java.js' diff --git a/.vitepress/sidebars/javalin.js b/.vitepress/sidebars/javalin.js new file mode 100644 index 0000000..8713934 --- /dev/null +++ b/.vitepress/sidebars/javalin.js @@ -0,0 +1,50 @@ +export const javalin = [ + { text: 'Introduction', link: '/javalin/index' }, + { + text: 'Data layer', + items: [ + { + text: 'Managing the database', + link: '/javalin/managing-the-database' + }, + { + text: 'Connecting to a database', + link: '/javalin/connecting-to-a-database' + }, + { + text: 'Adding a model layer', + link: '/javalin/adding-a-model-layer' + } + ] + }, + { + text: 'Creating an API', + items: [ + { text: 'Creating a server', link: '/javalin/creating-a-server' }, + { + text: 'Request and response', + link: '/javalin/request-response', + collapsed: true, + items: [ + { text: 'Query params', link: '/javalin/query-params' }, + { text: 'URL params', link: '/javalin/url-params' }, + { text: 'Body and headers', link: '/javalin/body-and-headers' } + ] + }, + { text: 'Routing', link: '/javalin/routing' }, + { text: 'Sending errors', link: '/javalin/sending-errors' }, + { text: 'Schema validation', link: '/javalin/schema-validation' } + ] + }, + { + text: 'Server side rendering', + items: [ + { text: 'Static files', link: '/javalin/static-files' }, + { text: 'Views and templates', link: '/javalin/views-and-templates' }, + { text: 'Using loops', link: '/javalin/using-loops' }, + { text: 'Template partials', link: '/javalin/template-partials' }, + { text: 'CSS and assets', link: '/javalin/css-and-assets' }, + { text: 'User input', link: '/javalin/user-input' } + ] + } +] diff --git a/.vitepress/sidebars/topnav.js b/.vitepress/sidebars/topnav.js index 3d3eb77..3e41d51 100644 --- a/.vitepress/sidebars/topnav.js +++ b/.vitepress/sidebars/topnav.js @@ -74,6 +74,15 @@ export const topnav = [ link: '/dotnet/' } ] + }, + { + text: 'Java', + items: [ + { + text: 'Javalin', + link: '/javalin/' + } + ] } ] }, diff --git a/src/javalin/adding-a-model-layer.md b/src/javalin/adding-a-model-layer.md new file mode 100644 index 0000000..1b988f2 --- /dev/null +++ b/src/javalin/adding-a-model-layer.md @@ -0,0 +1,64 @@ +# Adding a model layer + + + +## Creating models + +Models are responsible for interacting with the database. For example, we might +create a `User` class for interacting with the table of users: + +```js +// models/User.js +import db from '../db/index.js' + +class User { + static async findAll(limit, page) { + const query = 'SELECT * FROM users' + const results = await db.raw(query) + return results + } +} + +export default User +``` + +## Querying with substitutions + +Knex allows us to create SQL template queries with placeholders using `?` + +```js +static async findById(id) { + const query = `SELECT * FROM users WHERE id = ?` + const results = await db.raw(query, [id]) + return results[0] +} +``` + +::: danger + +Do not be tempted to interpolate raw arguments into the query string. This opens +you up to SQL injection attacks. + +Consider + +```js +User.findById('3; DROP TABLE users;') +``` + +Always use knex's `?` substitution syntax. + +::: + +## Inserting data + +We can use an `INSERT` query with several parameters by putting more `?` and +passing the substitutions in the array: + +```js +static async create(username, verified) { + const query = + 'INSERT INTO users (username, verified) VALUES (?, ?) RETURNING *' + const results = await db.raw(query, [username, verified]) + return results[0] +} +``` diff --git a/src/javalin/adding-css.md b/src/javalin/adding-css.md new file mode 100644 index 0000000..e69de29 diff --git a/src/javalin/auth.md b/src/javalin/auth.md new file mode 100644 index 0000000..e69de29 diff --git a/src/javalin/body-and-headers.md b/src/javalin/body-and-headers.md new file mode 100644 index 0000000..16cec95 --- /dev/null +++ b/src/javalin/body-and-headers.md @@ -0,0 +1,60 @@ +# Body and headers + + + +## Endpoints with body + +When we send a request, the headers are available in the backend as +`req.headers`. + +When we send a request with a body, it will be parsed into the `req.body` for us +on the backend. + +::: code-group + +```js{1} [server] +app.post('/users', async (req, res) => { + const user = await User.create(req.body.username, req.body.verified) + res.json(user) +}) +``` + +```bash{3} [client] +curl -v -X POST http://localhost:5000/users \ + -H "Content-Type: application/json" \ + -d '{"username": "MinnieMouse", "verified": true}' + +> POST /users HTTP/1.1 +> Host: localhost:5000 +> User-Agent: curl/7.81.0 +> Accept: */* +> Content-Type: application/json +> Content-Length: 45 + +< HTTP/1.1 200 OK +< X-Powered-By: Express +< Content-Type: application/json; charset=utf-8 +< Content-Length: 47 +< ETag: W/"2f-nb/7y2Be3oCM0RJlX39MzZ6dYkE" +< Date: Thu, 22 Feb 2024 16:57:57 GMT +< Connection: keep-alive +< Keep-Alive: timeout=5 + +{ + "id": 21, + "username": "MinnieMouse", + "verified": 1 +} + +``` + +::: + +::: tip + +According to the +[HTTP standard](https://www.rfc-editor.org/rfc/rfc9110.html#name-terminology-and-core-concep), +`GET` requests cannot have a body, so we generally only use body in requests +with `POST`, `PUT` and `PATCH`. + +::: diff --git a/src/javalin/connecting-to-a-database.md b/src/javalin/connecting-to-a-database.md new file mode 100644 index 0000000..9077794 --- /dev/null +++ b/src/javalin/connecting-to-a-database.md @@ -0,0 +1,47 @@ +# Connecting to the database + +## Setting up the connection + +We use JDBC to provide a connection to the database, allowing us to run SQL queries from Java. + +First, we add the `sqlite-jdbc` dependency to our `pom.xml`. + +```xml + + ... + + org.xerial + sqlite-jdbc + 3.46.0.0 + + +``` + +Then, we add our database connection code to `DB.java`. + +```js +import knex from 'knex' +import { fileURLToPath } from 'url' + +const uri = new URL('./db.sqlite', import.meta.url) + +const db = knex({ + client: 'sqlite3', + connection: { filename: fileURLToPath(uri) }, + useNullAsDefault: true +}) + +export default db +``` + +## Use the database connection + +Now we can run SQL queries on our database with `db.raw()`. + +For example, + +```js +const query = `SELECT * FROM users` +const results = await db.raw(query) +console.log(results) +``` diff --git a/src/javalin/creating-a-server.md b/src/javalin/creating-a-server.md new file mode 100644 index 0000000..2a2e73b --- /dev/null +++ b/src/javalin/creating-a-server.md @@ -0,0 +1,108 @@ +# Creating a server + + + +## Project setup + +To get our project set up, we will initialise a Node project and install some +dependencies: + +```bash +npm init -y +npm install express dotenv +npm install -D nodemon +``` + +We will create two files, one where we configure the server application, and one +where we run it: + +```bash +mkdir server +touch server/app.js +touch server/start.js +``` + +We also create the `.env` file and put global configuration constants in there: + +```bash +touch .env +``` + +```.env +PORT=5000 +``` + +## Configure the app + +In `server/app.js`, we configure the server application. + +```js +import express from 'express' + +const app = express() + +app.use(express.json()) + +app.get('/hello', (req, res) => { + res.json({ msg: 'Welcome to the Bleeter server!' }) +}) + +export default app +``` + +## Run the app + +In `server/start.js` we import and run the configured application. + +```js +import app from './app.js` + +const PORT = process.env.PORT + +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`) +}) +``` + +We add two scripts to `package.json` - `start` to run the server in production, +and `dev` to run the server in development. + +```json +"scripts": { + "start": "node server/start.js", + "dev": "nodemon server/start.js" +} +``` + +Now we can run the server with hot-reloading: + +::: code-group + +```bash +npm run dev +``` + +```console [output] +Server running on port 5000 +``` + +::: + +## Make a request + +Now we can start our server with `npm run dev` and make a `GET` request to +`localhost:5000/hello` to see the server return a message. + +::: code-group + +```bash +curl localhost:5000/hello +``` + +```console [output] +{ + msg: 'Welcome to the Bleeter server!' +} +``` + +::: diff --git a/src/javalin/css-and-assets.md b/src/javalin/css-and-assets.md new file mode 100644 index 0000000..d5e8576 --- /dev/null +++ b/src/javalin/css-and-assets.md @@ -0,0 +1,35 @@ +# CSS and assets + + + +## Including assets + +With static file serving set up, you are able to access assets in the +`public folder` from within your views. Suppose you have an image stored at +`public/images/logo.png`. You can display it in your views like this: + +```html{6} [index.ejs] +<%- include('partials/header') %> + +

Welcome to Bleeter

+

Follow the herd

+ +Bleeter logo + +<%- include('partials/footer') %> +``` + +## Applying CSS + +To apply CSS to your files, create a `.css` file in your public folder. For +example, `public/css/index.css`. To apply it, simply include in your HTML +`` as normal: + +```html{4} [header.ejs] + + + + + Bleeter | <%= title %> + +``` diff --git a/src/javalin/index.md b/src/javalin/index.md new file mode 100644 index 0000000..1b02e16 --- /dev/null +++ b/src/javalin/index.md @@ -0,0 +1,7 @@ +# Introduction + +

+ +![Express logo](image-1.png) + +

diff --git a/src/javalin/managing-the-database.md b/src/javalin/managing-the-database.md new file mode 100644 index 0000000..d3d161e --- /dev/null +++ b/src/javalin/managing-the-database.md @@ -0,0 +1,80 @@ +# Managing the database + +## Creating the schema + +We will set up our database with the following schema: + +::: code-group + +```sql [users] +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + verified BOOLEAN DEFAULT 0 +); +``` + +```sql [bleets] +CREATE TABLE IF NOT EXISTS bleets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL, + createdAt TEXT DEFAULT (datetime('now')), + userId INTEGER NOT NULL, + FOREIGN KEY (userId) REFERENCES USER(id) +); +``` + +```sql [likes] +CREATE TABLE IF NOT EXISTS user_bleet_likes ( + userId INTEGER NOT NULL, + bleetId INTEGER NOT NULL, + PRIMARY KEY (userId, bleetId), + FOREIGN KEY (userId) REFERENCES USER(id), + FOREIGN KEY (bleetId) REFERENCES BLEET(id) +); +``` + +::: + +## Creating the tables + +This SQL, along with the seed data, could be stored in the directory +`src/main/resources/db/migration` in our project. We _could_ run it with, for example, + +```bash +sqlite3 db.sqlite < src/main/resources/db/migration/V1__Reset.sql +``` + +However, we can use a tool called flyway to manage this. + +```xml + + + + org.flywaydb + flyway-maven-plugin + 10.15.2 + + jdbc:sqlite:nozama.db + + + + org.xerial + sqlite-jdbc + 3.46.0.0 + + + + + +``` + +```bash +mvn flyway:migrate +``` + +::: warning + +Since we're just practising, including your test seed data with your migrations is okay. + +::: diff --git a/src/javalin/middleware.md b/src/javalin/middleware.md new file mode 100644 index 0000000..4dd1086 --- /dev/null +++ b/src/javalin/middleware.md @@ -0,0 +1 @@ +# Middleware diff --git a/src/javalin/model-layer.md b/src/javalin/model-layer.md new file mode 100644 index 0000000..85b67e6 --- /dev/null +++ b/src/javalin/model-layer.md @@ -0,0 +1 @@ +# Adding a model layer diff --git a/src/javalin/partial-templates.md b/src/javalin/partial-templates.md new file mode 100644 index 0000000..e69de29 diff --git a/src/javalin/query-params.md b/src/javalin/query-params.md new file mode 100644 index 0000000..d5b7135 --- /dev/null +++ b/src/javalin/query-params.md @@ -0,0 +1,62 @@ +# Query params + + + +## URL structure + +If you've ever looked at the URL bar in your browser, you'll recognise query +params. They're the things separated by `&` and `?` after the url: + +```txt +https://www.google.com/search?q=how+to+css&&sclient=gws-wiz +``` + +## Parsing query params + +Express does a lot of the hard work for us. It parses the query params into an +object and attaches them to the `req` as `req.query`. + +::: code-group + +```js{2} [server] +app.get('/users', async (req, res) => { + const users = await User.findAll(req.query.limit) + res.json(users) +}) +``` + +```bash{1} [client] +> GET "/users?limit=3 HTTP/1.1" +> Host: localhost:5000 +> User-Agent: curl/7.81.0 +> Accept: */* + +< HTTP/1.1 200 OK +< X-Powered-By: Express +< Content-Type: application/json; charset=utf-8 +< Content-Length: 136 +< ETag: W/"88-4JxhISw0NhZK5Bpz0Tl1UP/kgq4" +< Date: Thu, 22 Feb 2024 16:42:33 GMT +< Connection: keep-alive +< Keep-Alive: timeout=5 + +[ + { + "id": 1, + "username": "EluskM", + "verified": 1 + }, + { + "id": 2, + "username": "BillGatekeeper", + "verified": 1 + }, + { + "id": 3, + "username": "JeffWho", + "verified": 0 + } +] +``` + +::: diff --git a/src/javalin/request-response.md b/src/javalin/request-response.md new file mode 100644 index 0000000..deefbb2 --- /dev/null +++ b/src/javalin/request-response.md @@ -0,0 +1,115 @@ +# Request and response + + + +## Request and response + +There are several ways to send extra information as part of the request and +response. These include: + +- Query params: parts of the url such as `?x=7&y=cool` +- URL params: dynamic parts of the endpoint, such as the `7` in `/users/7` +- Headers: Meta information about the request or response which is part of the + HTTP standard +- Body: Complex, structured data which accompanies the request or response + +## HTTP tools + +Tools such as [cURL](https://curl.se/docs/), [Postman](https://www.postman.com/) +and the VS Code plugin [Thunderclient](https://www.thunderclient.com/) allows us +to construct more complex requests to test our applications. They simulate +client applications such as CLIs, mobile apps and websites which would be making +these requests in production. + +## Sending and receiving JSON + +We can configure our app to send an receive JSON as part of the body. As a +bonus, this will also encode and parse between JSON and Javascript without need +of `JSON.parse()` and `JSON.stringify()`. + +```js{2} +const app = express() +app.use(express.json()) +``` + +Now we can send and receive json + +::: code-group + +```js [server] +app.get('/users', async (req, res) => { + const users = await User.findAll() + res.json(users) +}) +``` + +```bash [client] +> GET /users HTTP/1.1 +> Host: localhost:5000 +> User-Agent: curl/7.81.0 +> Accept: */* + +< HTTP/1.1 200 OK +< X-Powered-By: Express +< Content-Type: application/json; charset=utf-8 +< Content-Length: 465 +< ETag: W/"1d1-ANZR8XoyXChze/h4JDQ5/asCB28" +< Date: Thu, 22 Feb 2024 17:07:31 GMT +< Connection: keep-alive +< Keep-Alive: timeout=5 + +[ + { + "id": 1, + "username": "EluskM", + "verified": 1 + }, + { + "id": 2, + "username": "BillGatekeeper", + "verified": 1 + }, + { + "id": 3, + "username": "JeffWho", + "verified": 0 + }, + { + "id": 4, + "username": "OprahWindey", + "verified": 1 + }, + { + "id": 5, + "username": "MarkZeeberg", + "verified": 1 + }, + { + "id": 6, + "username": "LarryFlinger", + "verified": 0 + }, + { + "id": 7, + "username": "CannyWest", + "verified": 1 + }, + { + "id": 8, + "username": "TaylorSquid", + "verified": 0 + }, + { + "id": 9, + "username": "ArianaVenti", + "verified": 1 + }, + { + "id": 10, + "username": "KylieGenner", + "verified": 0 + } +] +``` + +::: diff --git a/src/javalin/routing.md b/src/javalin/routing.md new file mode 100644 index 0000000..2ba559e --- /dev/null +++ b/src/javalin/routing.md @@ -0,0 +1,112 @@ +# Routing + + + +## Creating a router + +To create an express router, we import the `Router` function from express. + +```js +import { Router } from 'express' +const router = Router() +``` + +## Adding endpoints + +We can add endpoints to `router` just like we do to `app`, by using the +`.get()`, `.post()`, `.put()` and `.delete()` methods. + +```js +router.get('/:userId', async (req, res) => { + const { userId } = req.params + const user = await User.findById(userId) + res.json(user) +}) +``` + +## Attaching the router + +We attach our router to `app` using `app.use()`. + +```js +app.use('/users', userRouter) +``` + +Notice that the `/users` prefix is added to all routes in the `userRouter`, +resulting in `/users/:userId` and so on. + +## Multiple routers + +We usually use routers to split our API routes into separate files, making the +project easier to maintain. + +::: code-group + +```js [users.js] +import { Router } from 'express' +import User from '../models/User.js' + +const router = Router() + +router.get('/', async (req, res) => { + const { limit } = req.query + const users = await User.findAll(limit) + res.json(users) +}) + +router.get('/:userId', async (req, res) => { + const { userId } = req.params + const user = await User.findById(userId) + res.json(user) +}) + +router.post('/', async (req, res) => { + const { username, verified } = req.body + const user = await User.create(username, verified) + res.json(user) +}) + +export default router +``` + +```js [bleets.js] +import { Router } from 'express' +import Bleet from '../models/Bleet.js' + +const router = Router() + +router.get('/', async (req, res) => { + const bleets = await Bleet.findAll() + res.json(bleets) +}) + +router.get('/:bleetId', async (req, res) => { + const { bleetId } = req.params + const bleet = await Bleet.findById(bleetId) + res.json(bleet) +}) + +router.post('/', async (req, res) => { + const { content, userId } = req.body + const bleet = await Bleet.create(content, userId) + res.json(bleet) +}) + +export default router +``` + +```js [app.js] +import express from 'express' +import userRouter from '../routers/users.js' +import bleetRouter from '../routers/bleets.js' + +const app = express() +app.use(express.json()) + +app.use('/users', userRouter) +app.use('/bleets', bleetRouter) + +export default app +``` + +::: diff --git a/src/javalin/schema-validation.md b/src/javalin/schema-validation.md new file mode 100644 index 0000000..ae61722 --- /dev/null +++ b/src/javalin/schema-validation.md @@ -0,0 +1,46 @@ +# Schema validation + + + +## Validation libraries + +When a client sends us a large object, checking each property one by one is +tedious. There are libraries such as Yup and Joi to help with this. We'll be +using Zod. + +## Creating a schema + +We can create a schema using Zod. + +```js +import { z } from 'zod' + +const UserSchema = z.object({ + username: z.string(), + firstName: z.string(), + lastName: z.string(), + email: z.string().email(), + avatar: z.string().optional(), + password: z.string() +}) +``` + +## Validating data + +When a user sends us some data (which we will call the `payload`) we need to +check that it matches the schema, i.e. that it has the correct properties and +that they are of the correct type. + +```js +const valid = UserSchema.safeParse(payload).success + +if (!valid) { + throw new AppError('User data is not valid.', 400) +} + +// if the error is not thrown, we can be confident that +// the payload has the correct properties +``` + +Do check out the [Zod docs](https://zod.dev/) to find out more about what it can +do. diff --git a/src/javalin/sending-errors.md b/src/javalin/sending-errors.md new file mode 100644 index 0000000..76d05b1 --- /dev/null +++ b/src/javalin/sending-errors.md @@ -0,0 +1,111 @@ +# Sending errors + + + +## Error codes + +When a client makes an HTTP request to the server, the server usually responds +with a status code. + +Here are some of the more common status codes. + +| Code | Status | Meaning | +| :--: | :-------------------- | :------------------------------------------------------------------------------- | +| 200 | OK | The operation has succeeded | +| 201 | Created | An entity has been created (e.g. rows inserted into the database) | +| 204 | No Content | The operation has succeeded, and the client doesn't need to do anything about it | +| 400 | Bad Request | The data sent by the client is not valid | +| 401 | Unauthorized | In order to access this endpoint, you need to send credentials | +| 403 | Forbidden | The credentials you send are not correct for this endpoint | +| 404 | Not Found | This resource is not found | +| 500 | Internal Server Error | Something went wrong and the server will not explain why | + +Detailed information on all status codes can be found on +[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status). + +In particular, we are interested in the error codes, (`400`-`599`). + +## Custom error class + +To allow us to throw errors with status codes, we can extend the `Error` class. + +```js +class AppError extends Error { + constructor(message, code) { + super(message) + this.code = code + } +} +``` + +## Throwing custom errors + +In our models, we can throw errors that arise from handling data, which is where +most errors occur. + +```js{2-4,9-11} +async findById(id) { + if (isNaN(id)) { + throw new AppError('ID must be a number.', 400) + } + + const query = 'SELECT * FROM users WHERE id = ?' + const results = await db.raw(query, [id]) + + if (!results.length) { + throw new AppError(`User with id ${id} does exist.`, 404) + } + + return results[0] +} +``` + +## Catching errors + +We can catch the error in our controller and decide what to do with it. In this +case, we pass the error to an error handler. + +```js +router.get('/:userId', async (req, res, next) => { + const { userId } = req.params + try { + const user = await User.findById(userId) + res.json(user) + } catch (err) { + next(err) + } +}) +``` + +## Handling errors + +We can send the error code and a useful message to the client by attaching an +error handler to the `app`. + +::: warning + +We should ensure the message doesn't expose our server's internal workings too +much, as this information could be used by hackers. Better not to send the whole +error object, but rather take full control of the information which is sent to +the client. + +::: + +```js +app.use((err, req, res, next) => { + console.error(err) + if (err instanceof AppError) { + res.status(err.code).json({ error: err.message }) + } else { + res.status(500).json({ error: 'Something went wrong.' }) + } +}) +``` + +::: tip + +Make sure that this `.use()` is the last method called on `app` before +`.listen()`. It must accept all 4 parameters `(err, req, res, next)` as this is +how express infers that the function is an error handler. + +::: diff --git a/src/javalin/static-files.md b/src/javalin/static-files.md new file mode 100644 index 0000000..f0724bd --- /dev/null +++ b/src/javalin/static-files.md @@ -0,0 +1,49 @@ +# Static files + + + +## Serving static files + +Until now, we have been using our server to send back data such as strings or +json. However, we can serve whole files such as pictures or documents, too! + +## Public directory + +Although you can put the files you'd like to serve anywhere, it's convention to +put them in a folder called `public` + +``` +public +├── logo.png +└── logs + └── hello.txt +``` + +## Using `express.static()` + +We instruct our express app to serve files from `public` using +`express.static()`. + +```js{11-12} +import express from 'express' +import api from '../api/index.js' +import { errorHandler } from './errors.js' + +const app = express() + +// configure api +app.use(express.json()) +app.use('/api', api) + +// serve static files +app.use(express.static('public')) + +app.use(errorHandler) +export default app + +``` + +## Requesting static files + +Now, if we visit `/logo.png` or `/logs/hello.txt` we will get the corresponding +files. Note that `/public/` is _not_ part of the url. diff --git a/src/javalin/template-partials.md b/src/javalin/template-partials.md new file mode 100644 index 0000000..87def5a --- /dev/null +++ b/src/javalin/template-partials.md @@ -0,0 +1,168 @@ +# Template partials + + + +## The problem + +Many pages in a website might share the same content. For example, our views +might look like this (the repeated content has been highlighted): + +::: code-group + +```html{1-9,12-13} [index.ejs] + + + + + + Bleeter | <%= title %> + + + +

Welcome to Bleeter

+

Follow the herd

+ + +``` + +```html{1-9,20-21} [bleets.ejs] + + + + + + Bleeter | <%= title %> + + + +

Bleets

+
    + <% for (let bleet of bleets) { %> + +
  1. + <%= bleet.content.slice(0, 10) + '...' %> + Read more +
  2. + <% } %> +
+ + +``` + +```html{1-9,13-14} [bleet.ejs] + + + + + + Bleeter | <%= title %> + + + +

A bleet

+

<%= bleet.content %>

+

<%= bleet.createdAt %>

+ + +``` + +::: + +This is a problem, because if we need to make a change to the shared parts of +each view, we need to update each view individually, which is extra work and an +opportunity for mistakes. + +## Creating partials + +We can create partial templates and use them in our views. + +The directory structure might look like this: + +```txt +views/ +├── bleet.ejs +├── bleets.ejs +├── index.ejs +└── partials + ├── footer.ejs + └── header.ejs +``` + +We will put the repeated HTML into the partial templates. + +::: code-group + +```html [header.ejs] + + + + + + Bleeter | <%= title %> + + + +``` + +```html [footer.ejs] + + +``` + +::: + +## Using partials + +To use the partial templates in our views, we use the EJS `include` function. + +::: code-group + +```html [index.ejs] +<%- include('partials/header') %> + +

Welcome to Bleeter

+

Follow the herd

+ +<%- include('partials/footer') %> +``` + +```html [bleets.ejs] +<%- include('partials/header') %> + +

Bleets

+
    + <% for (let bleet of bleets) { %> + +
  1. + <%= bleet.content.slice(0, 10) + '...' %> + Read more +
  2. + <% } %> +
+ +<%- include('partials/footer') %> +``` + +```html [bleet.ejs] +<%- include('partials/header') %> + +

A bleet

+

<%= bleet.content %>

+

<%= bleet.createdAt %>

+ +<%- include('partials/footer') %> +``` + +::: + +::: danger + +When rendering untrusted content such as user input (including input stored in the database), always use `<%= %>` and not `<%- %>`. The ensures that the HTML content is escaped and prevents [XSS attacks](https://learn.snyk.io/lesson/xss/). + +::: + +::: info + +Partials have access to the data of their parent! So, for example, the `title` which is passed to the `index.ejs` template is available in the `header.ejs` partial. + +::: \ No newline at end of file diff --git a/src/javalin/url-params.md b/src/javalin/url-params.md new file mode 100644 index 0000000..23a9721 --- /dev/null +++ b/src/javalin/url-params.md @@ -0,0 +1,64 @@ +# URL params + + + +## URL structure + +To make parts of the url dynamic in express, we precede them with a colon. They +will be parse into `req.params` for us by express. + +```js +app.get('/users/:x/:y/:z', (req, res) => { + console.log(req.params) +}) +``` + +Now, a `GET` request to `/users/3/awesome/true` would log + +```console +{ + x: '3', + y: 'awesome', + z: 'true' +} +``` + +to the console. + +## Using URL params + +A practical application of URL params is to create dynamic endpoints, where +users of the API can request particular rows from a table. + +::: code-group + +```js{2} [server] +app.get('/users/:userId', async (req, res) => { + const user = await User.findById(req.params.userId) + res.json(user) +}) +``` + +```bash{1} [client] +> GET /users/13 HTTP/1.1 +> Host: localhost:5000 +> User-Agent: curl/7.81.0 +> Accept: */* + +< HTTP/1.1 200 OK +< X-Powered-By: Express +< Content-Type: application/json; charset=utf-8 +< Content-Length: 46 +< ETag: W/"2e-kXyTSzYpI5ic6GsVpLyJPBxDD0E" +< Date: Thu, 22 Feb 2024 16:50:22 GMT +< Connection: keep-alive +< Keep-Alive: timeout=5 + +{ + "id": 13, + "username": "EdShearing", + "verified": 1 +} +``` + +::: diff --git a/src/javalin/user-input.md b/src/javalin/user-input.md new file mode 100644 index 0000000..3603276 --- /dev/null +++ b/src/javalin/user-input.md @@ -0,0 +1,78 @@ +# User input + + + +## Creating a form + +We create a view which displays a form to the user. + +```js +
+ + + + + + + + +
+``` + +::: info + +The `name` attribute of the inputs determines the key once the form data is +parsed into a javascript object on the server. This form would result in an +object with keys of `userId` and `content`. + +::: + +## Rendering the form + +We need to create a controller which renders the form for the user to complete. + +```js +web.get('/bleets/new', (req, res) => { + res.render('bleets/new', { title: 'Create a bleet' }) +}) +``` + +::: warning + +This controller must be placed _above_ the `/bleets/:id` endpoint controller. If +not, then `/bleets/new` will match the `bleets/:id` pattern, and the string +`new` will be interpreted as the `id` of a bleet and passed to the wrong +controller! + +::: + +## Parsing form data + +We must configure the app to properly parse form data into a javascript object +and make it available on the `req.body`. + +```js [app.js] +app.use(express.urlencoded({ extended: true })) +``` + +::: warning + +This must occur _before_ any of the web routes are attached to the `app`. + +::: + +## Handling the post request + +The form submit a `POST` request to the `/bleets` endpoint. We must create a +controller for this: + +```js +app.post('/bleets', async (req, res) => { + const { content, userId } = req.body + await Bleet.create(content, userId) + res.redirect('/bleets') +}) +``` + +The `res.redirect('/bleets')` sends to browser to the `/bleets` page on +successful submission. diff --git a/src/javalin/using-loops.md b/src/javalin/using-loops.md new file mode 100644 index 0000000..9a3a06c --- /dev/null +++ b/src/javalin/using-loops.md @@ -0,0 +1,55 @@ +# Using loops + + + +## Passing an array + +It is a very common scenario to pass an array of objects to our template. + +```js +app.get('/bleets', async (req, res) => { + const bleets = await Bleet.findAll() // bleets is an array + res.render('bleets', { bleets }) +}) +``` + +This renders the `bleets.ejs` template with access to the array of `bleets` from +the database. + +## Rendering lists + +In our view, we can use EJS to render the list using a `for`/`of` loop. + +```html +
    + <% for (let bleet of bleets) { %> +
  1. + <%= bleet.content.slice(0, 10) + '...' %> + Read more +
  2. + <% } %> +
+``` + +::: tip + +It is not necessary to use `
    ` or `
      ` when rendering lists - you can use +this looping pattern to render any html you like. + +::: + +::: info + +The `<% %>` tags interpolate javascript into the template without displaying +anything in the view. This is often used to start and end loops, or to do +conditional rendering such as + +```html +<% if (condition) { %> +

      Condition is true!

      +<% } else { %> +

      Condition is false!

      +<% } %> +``` + +::: diff --git a/src/javalin/views-and-static.md b/src/javalin/views-and-static.md new file mode 100644 index 0000000..65cd0e7 --- /dev/null +++ b/src/javalin/views-and-static.md @@ -0,0 +1,85 @@ +# Views with EJS + +So far, we have used express to send JSON. But it can do much more than that. +Dynamic web applications often require the ability to generate HTML content +dynamically, based on the server-side data. EJS (Embedded JavaScript) templates +allow us to do just that in an Express application. In this section, we will +learn how to set up and use EJS as our templating engine to render dynamic +views. + +## Adding a view engine + +A view engine allows us to send this html to the user. + +First, we install `ejs`. + +```bash +npm install ejs +``` + +The, in `npm/app.js`, we register EJS as our view engine. + +```js +// server/app.js +app.set('view engine', 'ejs') +``` + +## Creating views + +Let's make a folder for our views, and create our first view. + +```bash +mkdir views +touch views/index.ejs +``` + +A `.ejs` file is basically HTML with some added javascript to make it dynamic. +In `index.ejs`, let's add the following: + +```html + + + + + + Bleeter + + +
      Bleeter
      +
      +

      Users

      +
      +
      Copyright
      + + +``` + +Now, in `app.js`, we'll add + +```js +app.get('/', (req, res) => { + res.render('index') +}) +``` + +The `.render` method will look for `index.js` in the `views` folder and send it +to the browser. + +## Including static files + +If we would like to include some `css` without our view, it means sending an +additional "static" file. Fortunately, sending static files in express is quite +easy. We add this line to `app.js` + +```js +app.use(express.static('public')) +``` + +Now, we can put our `style.css` in the `public` folder and add the following +line to the `` of our `index.js` + +```html + +``` + +We have created our first web server! diff --git a/src/javalin/views-and-templates.md b/src/javalin/views-and-templates.md new file mode 100644 index 0000000..119fc9c --- /dev/null +++ b/src/javalin/views-and-templates.md @@ -0,0 +1,55 @@ +# Views and templates + + + +## Adding a template engine + +First, we install `ejs` in our project + +```bash +npm install ejs +``` + +Then, we configure our app to render `.ejs` files found in the `views` folder to +`.html` before sending them to the browser. + +```js +app.set('view engine', 'ejs') +``` + +## Adding an endpoint + +We configure endpoints to determine which URLs serve which views. + +```js +app.get('/', async (req, res) => { + const bleet = await Bleet.findById(3) + + res.render('index', { + bleet + }) +}) +``` + +Notice we are using `res.render` rather than `res.send` or `res.json`. + +## Creating an EJS file + +Finally, in the `views` directory, we create our `index.ejs`. + +```html + + + + + + Bleeter + + + +

      Welcome to Bleeter!

      +

      Read some bleets...

      +

      <%= bleet.content %>

      + + +``` From ec500b02c51a8c9580f57fd6d34a55012ea1b22c Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Wed, 17 Jul 2024 00:35:41 +0000 Subject: [PATCH 04/13] Write docs on connecting to a db --- src/javalin/adding-a-model-layer.md | 47 ++++--- src/javalin/connecting-to-a-database.md | 169 +++++++++++++++++++----- src/javalin/managing-the-database.md | 16 +-- 3 files changed, 172 insertions(+), 60 deletions(-) diff --git a/src/javalin/adding-a-model-layer.md b/src/javalin/adding-a-model-layer.md index 1b988f2..8c4a397 100644 --- a/src/javalin/adding-a-model-layer.md +++ b/src/javalin/adding-a-model-layer.md @@ -1,34 +1,39 @@ -# Adding a model layer +# Adding a data access layer - +We'll need to create the "Data Access Layer". That is, the part of our application that deals with interacting with the database and data models. -## Creating models +## Models and Repositories -Models are responsible for interacting with the database. For example, we might -create a `User` class for interacting with the table of users: +Models are classes that we use to store data within the application. + +```java +package com.corndel.bleeter.Models; + +public class User { -```js -// models/User.js -import db from '../db/index.js' - -class User { - static async findAll(limit, page) { - const query = 'SELECT * FROM users' - const results = await db.raw(query) - return results - } } +``` + +Repositories are classes that interact with the database to let us persist, modify, and delete this data. -export default User + +```java +package com.corndel.bleeter.Repositories; + +import com.corndel.bleeter.Models.User; + +public class UserRepository { + +} ``` ## Querying with substitutions -Knex allows us to create SQL template queries with placeholders using `?` +JDBC lets us set up _Prepared Statements_. These let us substitute in parameters to our SQL queries. ```js -static async findById(id) { - const query = `SELECT * FROM users WHERE id = ?` +static User findById(id) { + var query = `SELECT * FROM users WHERE id = ?` const results = await db.raw(query, [id]) return results[0] } @@ -42,10 +47,10 @@ you up to SQL injection attacks. Consider ```js -User.findById('3; DROP TABLE users;') + User.findById('3; DROP TABLE users;') ``` -Always use knex's `?` substitution syntax. +Always use prepared statements! ::: diff --git a/src/javalin/connecting-to-a-database.md b/src/javalin/connecting-to-a-database.md index 9077794..e29f7b2 100644 --- a/src/javalin/connecting-to-a-database.md +++ b/src/javalin/connecting-to-a-database.md @@ -1,47 +1,156 @@ # Connecting to the database -## Setting up the connection +## Setting up the database driver -We use JDBC to provide a connection to the database, allowing us to run SQL queries from Java. +We use Java's built-in [JDBC](https://en.wikipedia.org/wiki/Java_Database_Connectivity) library to provide a connection to the database, allowing us to run SQL queries from Java. -First, we add the `sqlite-jdbc` dependency to our `pom.xml`. +First, we _add_ the `sqlite-jdbc` dependency to our `pom.xml`. -```xml - - ... - - org.xerial - sqlite-jdbc - 3.46.0.0 - - +```xml{3-7} + + + + org.xerial + sqlite-jdbc + 3.46.0.0 + + + + ``` -Then, we add our database connection code to `DB.java`. +This lets JDBC connect to our sqlite database. -```js -import knex from 'knex' -import { fileURLToPath } from 'url' +## Use the database connection + +We can then make queries to the database like so: + +```java +package com.corndel.bleeter.Repositories; + +import com.corndel.bleeter.Models.User; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; -const uri = new URL('./db.sqlite', import.meta.url) +public class UserRepository { + public static List findAll() throws SQLException { + var dbUrl = "jdbc:sqlite:bleeter.db"; // [!code focus:7] + var query = + "SELECT id, username, firstName, lastName, email, avatar FROM users"; -const db = knex({ - client: 'sqlite3', - connection: { filename: fileURLToPath(uri) }, - useNullAsDefault: true -}) + try (var connection = DriverManager.getConnection(dbUrl); // [!code highlight:3] + var statement = connection.createStatement(); + var resultSet = statement.executeQuery(query);) { -export default db + var users = new ArrayList(); + while (resultSet.next()) { + var id = resultSet.getInt("id"); + var username = resultSet.getString("username"); + var firstName = resultSet.getString("firstName"); + var lastName = resultSet.getString("lastName"); + var email = resultSet.getString("email"); + var avatar = resultSet.getString("avatar"); + + users.add(new User(id, username, firstName, lastName, email, avatar)); + } + + return users; + } + } +} ``` -## Use the database connection +::: info + +Notice the assignments in the highlighted `try` statement. + +This is a [_try-with-resources_ statement](https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html). It will automatically `.close()` the `resultSet`, `statement`, and `connection` once the code leaves the try block. + +This is important because the database connection and related objects can tie up system resources, and `.close()`-ing frees these up so other parts of your system can use them again! + +::: + + +## Create a Database connection class + +Imagine if we have _hundreds_ of queries! It could become a hassle if we need to change the way we make connections to the database. We can create a class to manage these connections instead. + +```java +package com.corndel.bleeter; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class DB { + static final String dbUrl = "jdbc:sqlite:bleeter.db"; + + public static Connection getConnection() throws SQLException { + return DriverManager.getConnection(dbUrl); + } +} +``` + +Now we can make a connection through this class instead of having to enter the connection string every time we want to make a query. + +For example, we've made a tiny change to the code above: + +```java +package com.corndel.bleeter.Repositories; + +import com.corndel.bleeter.Models.User; +import com.corndel.bleeter.DB; // [!code ++] +import java.sql.DriverManager; // [!code --] +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class UserRepository { + public static List findAll() throws SQLException { // [!code focus:23] + var dbUrl = "jdbc:sqlite:bleeter.db"; // [!code --] + var query = + "SELECT id, username, firstName, lastName, email, avatar FROM users"; + + try (var connection = DriverManager.getConnection(dbUrl); // [!code --] + try (var connection = DB.getConnection(); // [!code ++] + var statement = connection.createStatement(); + var resultSet = statement.executeQuery(query);) { + + var users = new ArrayList(); + while (resultSet.next()) { + var id = resultSet.getInt("id"); + var username = resultSet.getString("username"); + var firstName = resultSet.getString("firstName"); + var lastName = resultSet.getString("lastName"); + var email = resultSet.getString("email"); + var avatar = resultSet.getString("avatar"); + + users.add(new User(id, username, firstName, lastName, email, avatar)); + } + + return users; + } + } +} +``` + +So that, if we needed to change the `dbUrl` for some reason, we need only do that in one place! + +```java +package com.corndel.bleeter; -Now we can run SQL queries on our database with `db.raw()`. +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; -For example, +public class DB { + static final String dbUrl = "jdbc:sqlite:bleeter.db"; // [!code --] + static final String dbUrl = "jdbc:sqlite:some-other-file.db"; // [!code ++] -```js -const query = `SELECT * FROM users` -const results = await db.raw(query) -console.log(results) + public static Connection getConnection() throws SQLException { + return DriverManager.getConnection(dbUrl); + } +} ``` diff --git a/src/javalin/managing-the-database.md b/src/javalin/managing-the-database.md index d3d161e..5e9f476 100644 --- a/src/javalin/managing-the-database.md +++ b/src/javalin/managing-the-database.md @@ -42,10 +42,12 @@ This SQL, along with the seed data, could be stored in the directory `src/main/resources/db/migration` in our project. We _could_ run it with, for example, ```bash -sqlite3 db.sqlite < src/main/resources/db/migration/V1__Reset.sql +sqlite3 bleeter.sqlite < src/main/resources/db/migration/V1__Reset.sql ``` -However, we can use a tool called flyway to manage this. +However, we can instead use a tool called flyway to manage this. + +In `pom.xml`, we can configure the flyway plugin to manage the correct database. ```xml @@ -55,7 +57,7 @@ However, we can use a tool called flyway to manage this. flyway-maven-plugin 10.15.2 - jdbc:sqlite:nozama.db + jdbc:sqlite:bleeter.sqlite @@ -69,12 +71,8 @@ However, we can use a tool called flyway to manage this. ``` +We can then use flyway to run the sql statements in order using the following commmand: + ```bash mvn flyway:migrate ``` - -::: warning - -Since we're just practising, including your test seed data with your migrations is okay. - -::: From 3344366110f94e88a07ed431754e24dbbd13ded9 Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Wed, 17 Jul 2024 00:39:52 +0000 Subject: [PATCH 05/13] fiddle about with code focus --- src/javalin/connecting-to-a-database.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/javalin/connecting-to-a-database.md b/src/javalin/connecting-to-a-database.md index e29f7b2..0f6ab12 100644 --- a/src/javalin/connecting-to-a-database.md +++ b/src/javalin/connecting-to-a-database.md @@ -52,10 +52,8 @@ public class UserRepository { var lastName = resultSet.getString("lastName"); var email = resultSet.getString("email"); var avatar = resultSet.getString("avatar"); - users.add(new User(id, username, firstName, lastName, email, avatar)); } - return users; } } @@ -84,7 +82,7 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; -public class DB { +public class DB { // [!code focus:7] static final String dbUrl = "jdbc:sqlite:bleeter.db"; public static Connection getConnection() throws SQLException { @@ -108,8 +106,8 @@ import java.util.ArrayList; import java.util.List; public class UserRepository { - public static List findAll() throws SQLException { // [!code focus:23] - var dbUrl = "jdbc:sqlite:bleeter.db"; // [!code --] + public static List findAll() throws SQLException { + var dbUrl = "jdbc:sqlite:bleeter.db"; // [!code --] // [!code focus:8] var query = "SELECT id, username, firstName, lastName, email, avatar FROM users"; @@ -117,7 +115,6 @@ public class UserRepository { try (var connection = DB.getConnection(); // [!code ++] var statement = connection.createStatement(); var resultSet = statement.executeQuery(query);) { - var users = new ArrayList(); while (resultSet.next()) { var id = resultSet.getInt("id"); @@ -126,10 +123,8 @@ public class UserRepository { var lastName = resultSet.getString("lastName"); var email = resultSet.getString("email"); var avatar = resultSet.getString("avatar"); - users.add(new User(id, username, firstName, lastName, email, avatar)); } - return users; } } @@ -145,7 +140,7 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; -public class DB { +public class DB { // [!code focus:8] static final String dbUrl = "jdbc:sqlite:bleeter.db"; // [!code --] static final String dbUrl = "jdbc:sqlite:some-other-file.db"; // [!code ++] From 1048a0403f71ed36068b21aaf7f9cc817439ef73 Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Wed, 17 Jul 2024 01:20:57 +0000 Subject: [PATCH 06/13] wip --- src/javalin/adding-a-model-layer.md | 83 +++++++++++++++++++------ src/javalin/connecting-to-a-database.md | 33 ++++------ 2 files changed, 78 insertions(+), 38 deletions(-) diff --git a/src/javalin/adding-a-model-layer.md b/src/javalin/adding-a-model-layer.md index 8c4a397..e4badd0 100644 --- a/src/javalin/adding-a-model-layer.md +++ b/src/javalin/adding-a-model-layer.md @@ -7,10 +7,22 @@ We'll need to create the "Data Access Layer". That is, the part of our applicati Models are classes that we use to store data within the application. ```java -package com.corndel.bleeter.Models; +package com.corndel.bleeter.models; public class User { - + private Integer id; + public String username; + public boolean verified; + + public User(Integer id, String username, boolean verified) { + this.id = id; + this.username = username; + this.verified = verified; + } + + public Integer getId() { + return id; + } } ``` @@ -18,12 +30,32 @@ Repositories are classes that interact with the database to let us persist, modi ```java -package com.corndel.bleeter.Repositories; +package com.corndel.bleeter.repositories; -import com.corndel.bleeter.Models.User; +import com.corndel.bleeter.models.User; +import com.corndel.bleeter.DB; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; public class UserRepository { - + public static List findAll() throws SQLException { + var query = "SELECT id, username, verified FROM users"; + + try (var connection = DB.getConnection(); + var statement = connection.createStatement(); + var resultSet = statement.executeQuery(query);) { + + var users = new ArrayList(); + while (resultSet.next()) { + var id = resultSet.getInt("id"); + var username = resultSet.getString("username"); + var verified = resultSet.getBoolean("verified"); + users.add(new User(id, username, verified)); + } + return users; + } + } } ``` @@ -31,11 +63,22 @@ public class UserRepository { JDBC lets us set up _Prepared Statements_. These let us substitute in parameters to our SQL queries. -```js -static User findById(id) { - var query = `SELECT * FROM users WHERE id = ?` - const results = await db.raw(query, [id]) - return results[0] +```java +public static User findById(id) { + var query = "SELECT id, username, verified FROM users WHERE id = ?"; // [!code highlight:7] + try (var connection = DB.getConnection(); + var statement = connection.prepareStatement(query)) { + statement.setInt(1, id) + try (var resultSet = statement.executeQuery()) { + if (!resultSet.next()) { + return null; + } + var id = resultSet.getInt("id"); + var username = resultSet.getString("username"); + var verified = resultSet.getBoolean("verified"); + return new User(id, username, verified); + } + } } ``` @@ -46,8 +89,8 @@ you up to SQL injection attacks. Consider -```js - User.findById('3; DROP TABLE users;') +```java + User.findById("3; DROP TABLE users;"); ``` Always use prepared statements! @@ -59,11 +102,15 @@ Always use prepared statements! We can use an `INSERT` query with several parameters by putting more `?` and passing the substitutions in the array: -```js -static async create(username, verified) { - const query = - 'INSERT INTO users (username, verified) VALUES (?, ?) RETURNING *' - const results = await db.raw(query, [username, verified]) - return results[0] +```java +public static User create(username, verified) { + var query = "INSERT INTO users (username, verified) VALUES (?, ?) RETURNING *"; + } ``` + +::: info + +Note the `RETURNING *` + +::: diff --git a/src/javalin/connecting-to-a-database.md b/src/javalin/connecting-to-a-database.md index 0f6ab12..5f17ba1 100644 --- a/src/javalin/connecting-to-a-database.md +++ b/src/javalin/connecting-to-a-database.md @@ -26,9 +26,9 @@ This lets JDBC connect to our sqlite database. We can then make queries to the database like so: ```java -package com.corndel.bleeter.Repositories; +package com.corndel.bleeter.repositories; -import com.corndel.bleeter.Models.User; +import com.corndel.bleeter.models.User; import java.sql.DriverManager; import java.sql.SQLException; import java.util.ArrayList; @@ -36,9 +36,8 @@ import java.util.List; public class UserRepository { public static List findAll() throws SQLException { - var dbUrl = "jdbc:sqlite:bleeter.db"; // [!code focus:7] - var query = - "SELECT id, username, firstName, lastName, email, avatar FROM users"; + var dbUrl = "jdbc:sqlite:bleeter.db"; // [!code focus:6] + var query = "SELECT id, username, verified FROM users"; try (var connection = DriverManager.getConnection(dbUrl); // [!code highlight:3] var statement = connection.createStatement(); @@ -48,11 +47,8 @@ public class UserRepository { while (resultSet.next()) { var id = resultSet.getInt("id"); var username = resultSet.getString("username"); - var firstName = resultSet.getString("firstName"); - var lastName = resultSet.getString("lastName"); - var email = resultSet.getString("email"); - var avatar = resultSet.getString("avatar"); - users.add(new User(id, username, firstName, lastName, email, avatar)); + var verified = resultSet.getBoolean("verified"); + users.add(new User(id, username, verified)); } return users; } @@ -96,9 +92,9 @@ Now we can make a connection through this class instead of having to enter the c For example, we've made a tiny change to the code above: ```java -package com.corndel.bleeter.Repositories; +package com.corndel.bleeter.repositories; -import com.corndel.bleeter.Models.User; +import com.corndel.bleeter.models.User; import com.corndel.bleeter.DB; // [!code ++] import java.sql.DriverManager; // [!code --] import java.sql.SQLException; @@ -107,23 +103,20 @@ import java.util.List; public class UserRepository { public static List findAll() throws SQLException { - var dbUrl = "jdbc:sqlite:bleeter.db"; // [!code --] // [!code focus:8] - var query = - "SELECT id, username, firstName, lastName, email, avatar FROM users"; + var dbUrl = "jdbc:sqlite:bleeter.db"; // [!code --] // [!code focus:7] + var query = "SELECT id, username, verified FROM users"; try (var connection = DriverManager.getConnection(dbUrl); // [!code --] try (var connection = DB.getConnection(); // [!code ++] var statement = connection.createStatement(); var resultSet = statement.executeQuery(query);) { + var users = new ArrayList(); while (resultSet.next()) { var id = resultSet.getInt("id"); var username = resultSet.getString("username"); - var firstName = resultSet.getString("firstName"); - var lastName = resultSet.getString("lastName"); - var email = resultSet.getString("email"); - var avatar = resultSet.getString("avatar"); - users.add(new User(id, username, firstName, lastName, email, avatar)); + var verified = resultSet.getBoolean("verified"); + users.add(new User(id, username, verified)); } return users; } From a81090bd7849144cc701fd3015206cfb3a6157d3 Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Wed, 17 Jul 2024 11:10:50 +0000 Subject: [PATCH 07/13] remove bad url --- src/javalin/adding-a-model-layer.md | 30 ++++++++++++++++++++--------- src/javalin/index.md | 2 -- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/javalin/adding-a-model-layer.md b/src/javalin/adding-a-model-layer.md index e4badd0..5bd8789 100644 --- a/src/javalin/adding-a-model-layer.md +++ b/src/javalin/adding-a-model-layer.md @@ -1,10 +1,10 @@ -# Adding a data access layer +# Adding a model layer -We'll need to create the "Data Access Layer". That is, the part of our application that deals with interacting with the database and data models. +We'll need to create the _model_ layer and the _data access layer_. These are the parts of our application that deals with handling data models and persisiting data using the database. ## Models and Repositories -Models are classes that we use to store data within the application. +Models are classes that we use to represent data within the application. ```java package com.corndel.bleeter.models; @@ -65,10 +65,10 @@ JDBC lets us set up _Prepared Statements_. These let us substitute in parameters ```java public static User findById(id) { - var query = "SELECT id, username, verified FROM users WHERE id = ?"; // [!code highlight:7] + var query = "SELECT id, username, verified FROM users WHERE id = ?"; // [!code highlight] try (var connection = DB.getConnection(); - var statement = connection.prepareStatement(query)) { - statement.setInt(1, id) + var statement = connection.prepareStatement(query)) { // [!code highlight] + statement.setInt(1, id) // [!code highlight] try (var resultSet = statement.executeQuery()) { if (!resultSet.next()) { return null; @@ -100,17 +100,29 @@ Always use prepared statements! ## Inserting data We can use an `INSERT` query with several parameters by putting more `?` and -passing the substitutions in the array: +passing the substitutions in with `.setString()`, `.setInt()`, or the appropriate set method for the datatype: ```java public static User create(username, verified) { - var query = "INSERT INTO users (username, verified) VALUES (?, ?) RETURNING *"; + var query = // [!code highlight:2] + "INSERT INTO users (username, verified) VALUES (?, ?) RETURNING *"; + try (var connection = DB.getConnection(); + var statement = con.prepareStatement(query)) { + statement.setString(1, username); // [!code highlight] + statement.executeUpdate(); // [!code highlight] + + try (var resultSet = statement.getResultSet()) { + rs.next() + var id = rs.getInt("id"); + return new User(id, username, verified) + } + } } ``` ::: info -Note the `RETURNING *` +Note the `RETURNING *` causes the statement to have a `resultSet` after execution. This lets us get the `id` and other fields of the newly created `User` from the database. ::: diff --git a/src/javalin/index.md b/src/javalin/index.md index 1b02e16..d6a486b 100644 --- a/src/javalin/index.md +++ b/src/javalin/index.md @@ -2,6 +2,4 @@

      -![Express logo](image-1.png) -

      From 6e8149a47624dae96ebdc8990a961a2e16d0a425 Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Thu, 18 Jul 2024 11:12:55 +0000 Subject: [PATCH 08/13] translate query and path params --- src/javalin/creating-a-server.md | 116 ++++++++++++++----------------- src/javalin/query-params.md | 20 ++---- src/javalin/request-response.md | 23 ++---- src/javalin/routing.md | 2 - src/javalin/url-params.md | 63 ++++++++--------- 5 files changed, 97 insertions(+), 127 deletions(-) diff --git a/src/javalin/creating-a-server.md b/src/javalin/creating-a-server.md index 2a2e73b..a0607f9 100644 --- a/src/javalin/creating-a-server.md +++ b/src/javalin/creating-a-server.md @@ -1,108 +1,96 @@ # Creating a server - - ## Project setup -To get our project set up, we will initialise a Node project and install some -dependencies: - -```bash -npm init -y -npm install express dotenv -npm install -D nodemon -``` - -We will create two files, one where we configure the server application, and one -where we run it: +### Set up a Maven project -```bash -mkdir server -touch server/app.js -touch server/start.js -``` +qq -We also create the `.env` file and put global configuration constants in there: +### Add the Javalin dependency -```bash -touch .env -``` +Add the following dependency to `pom.xml` -```.env -PORT=5000 +```xml + + io.javalin + javalin + 6.1.6 + ``` ## Configure the app -In `server/app.js`, we configure the server application. +In `App.java`, we configure the Javalin application and the `main` entrypoint. -```js -import express from 'express' +```java +package com.corndel.bleeter; -const app = express() +import io.javalin.Javalin; -app.use(express.json()) +public class App { + private Javalin app; -app.get('/hello', (req, res) => { - res.json({ msg: 'Welcome to the Bleeter server!' }) -}) + public static void main(String[] args) { + var javalin = new App().javalinApp(); + javalin.start(8080); + } -export default app -``` + public App() { + app = Javalin.create(); + app.get("/hello", ctx -> ctx.result("Welcome to the Bleeter server!")); + } -## Run the app - -In `server/start.js` we import and run the configured application. - -```js -import app from './app.js` - -const PORT = process.env.PORT - -app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`) -}) -``` - -We add two scripts to `package.json` - `start` to run the server in production, -and `dev` to run the server in development. - -```json -"scripts": { - "start": "node server/start.js", - "dev": "nodemon server/start.js" + public Javalin javalinApp() { + return app; + } } ``` -Now we can run the server with hot-reloading: +## Compile and run the app + +We can now either use VSCode to compile and run our server, or run it in the command line with the following command: ::: code-group ```bash -npm run dev +./mvnw clean compile exec:java -Dexec.mainClass=com.corndel.bleeter.App ``` ```console [output] -Server running on port 5000 +17:23:14.039 [com.corndel.bleeter.App.main()] INFO io.javalin.Javalin - Starting Javalin ... +17:23:14.042 [com.corndel.bleeter.App.main()] INFO org.eclipse.jetty.server.Server - jetty-11.0.20; built: 2024-01-29T21:04:22.394Z; git: 922f8dc188f7011e60d0361de585fd4ac4d63064; jvm 21.0.3+9-LTS +17:23:14.088 [com.corndel.bleeter.App.main()] INFO o.e.j.s.s.DefaultSessionIdManager - Session workerName=node0 +17:23:14.100 [com.corndel.bleeter.App.main()] INFO o.e.j.server.handler.ContextHandler - Started o.e.j.s.ServletContextHandler@659d0500{/,null,AVAILABLE} +17:23:14.106 [com.corndel.bleeter.App.main()] INFO o.e.jetty.server.AbstractConnector - Started ServerConnector@69c25ba8{HTTP/1.1, (http/1.1)}{0.0.0.0:8080} +17:23:14.111 [com.corndel.bleeter.App.main()] INFO org.eclipse.jetty.server.Server - Started Server@54c69995{STARTING}[11.0.20,sto=0] @2212ms +17:23:14.111 [com.corndel.bleeter.App.main()] INFO io.javalin.Javalin - + __ ___ _____ + / /___ __ ______ _/ (_)___ / ___/ + __ / / __ `/ | / / __ `/ / / __ \ / __ \ +/ /_/ / /_/ /| |/ / /_/ / / / / / / / /_/ / +\____/\__,_/ |___/\__,_/_/_/_/ /_/ \____/ + + https://javalin.io/documentation + +17:23:14.111 [com.corndel.bleeter.App.main()] INFO io.javalin.Javalin - Javalin started in 122ms \o/ +17:23:14.114 [com.corndel.bleeter.App.main()] INFO io.javalin.Javalin - Listening on http://localhost:8080/ +17:23:14.120 [com.corndel.bleeter.App.main()] INFO io.javalin.Javalin - You are running Javalin 6.1.6 (released May 28, 2024). ``` ::: ## Make a request -Now we can start our server with `npm run dev` and make a `GET` request to -`localhost:5000/hello` to see the server return a message. +Now we can make a `GET` request to `localhost:8080/hello` to see the server return a message. ::: code-group ```bash -curl localhost:5000/hello +curl localhost:8080/hello ``` ```console [output] -{ - msg: 'Welcome to the Bleeter server!' -} +Welcome to the Bleeter server! ``` ::: diff --git a/src/javalin/query-params.md b/src/javalin/query-params.md index d5b7135..7ba7bc8 100644 --- a/src/javalin/query-params.md +++ b/src/javalin/query-params.md @@ -1,7 +1,5 @@ # Query params - - ## URL structure If you've ever looked at the URL bar in your browser, you'll recognise query @@ -13,32 +11,28 @@ https://www.google.com/search?q=how+to+css&&sclient=gws-wiz ## Parsing query params -Express does a lot of the hard work for us. It parses the query params into an -object and attaches them to the `req` as `req.query`. +Javalin does a lot of the hard work for us. We can access the query parameters through the context object using the `queryParam()` method. ::: code-group -```js{2} [server] -app.get('/users', async (req, res) => { - const users = await User.findAll(req.query.limit) - res.json(users) +```java{2,3} [server] +app.get("/users", ctx -> { + var limit = Integer.parseInt(ctx.queryParam("limit")); + var users = UserRepository.findAll(limit); + ctx.json(users); }) ``` ```bash{1} [client] > GET "/users?limit=3 HTTP/1.1" -> Host: localhost:5000 +> Host: localhost:8080 > User-Agent: curl/7.81.0 > Accept: */* < HTTP/1.1 200 OK -< X-Powered-By: Express < Content-Type: application/json; charset=utf-8 < Content-Length: 136 -< ETag: W/"88-4JxhISw0NhZK5Bpz0Tl1UP/kgq4" < Date: Thu, 22 Feb 2024 16:42:33 GMT -< Connection: keep-alive -< Keep-Alive: timeout=5 [ { diff --git a/src/javalin/request-response.md b/src/javalin/request-response.md index deefbb2..f9fd1a7 100644 --- a/src/javalin/request-response.md +++ b/src/javalin/request-response.md @@ -1,7 +1,5 @@ # Request and response - - ## Request and response There are several ways to send extra information as part of the request and @@ -23,24 +21,15 @@ these requests in production. ## Sending and receiving JSON -We can configure our app to send an receive JSON as part of the body. As a -bonus, this will also encode and parse between JSON and Javascript without need -of `JSON.parse()` and `JSON.stringify()`. - -```js{2} -const app = express() -app.use(express.json()) -``` - -Now we can send and receive json +We can send and receive JSON. ::: code-group -```js [server] -app.get('/users', async (req, res) => { - const users = await User.findAll() - res.json(users) -}) +```java [server] +app.get("/users", ctx -> { + var users = UserRepository.findAll(); + ctx.json(users); +}); ``` ```bash [client] diff --git a/src/javalin/routing.md b/src/javalin/routing.md index 2ba559e..f52b0ba 100644 --- a/src/javalin/routing.md +++ b/src/javalin/routing.md @@ -1,7 +1,5 @@ # Routing - - ## Creating a router To create an express router, we import the `Router` function from express. diff --git a/src/javalin/url-params.md b/src/javalin/url-params.md index 23a9721..c1d43af 100644 --- a/src/javalin/url-params.md +++ b/src/javalin/url-params.md @@ -1,59 +1,60 @@ # URL params - - ## URL structure -To make parts of the url dynamic in express, we precede them with a colon. They -will be parse into `req.params` for us by express. +To make parts of the url dynamic in Javalin, we can use curly braces like so: + +```java {1} +app.get("/users/{x}/{y}/{z}", ctx -> { + var x = ctx.pathParam("x"); + var y = ctx.pathParam("y"); + var z = ctx.pathParam("z"); + + var hashMap = new HashMap(); + + hashMap.put("x", x); + hashMap.put("y", y); + hashMap.put("z", z); -```js -app.get('/users/:x/:y/:z', (req, res) => { - console.log(req.params) + ctx.json(hashMap); }) ``` -Now, a `GET` request to `/users/3/awesome/true` would log +Now, a `GET` request to `/users/3/awesome/true` would respond with: -```console +``` { - x: '3', - y: 'awesome', - z: 'true' + "x": "3", + "y": "awesome", + "z": "true" } ``` -to the console. - ## Using URL params -A practical application of URL params is to create dynamic endpoints, where -users of the API can request particular rows from a table. +A practical application of URL params is to create dynamic endpoints, where users of the API can request particular rows from a table. ::: code-group -```js{2} [server] -app.get('/users/:userId', async (req, res) => { - const user = await User.findById(req.params.userId) - res.json(user) +```java{2-3} [server] +app.get("/users/{userId}", ctx -> { + var id = Integer.parseInt(ctx.pathParam("userId")); + var user = UserRepository.findById(id); + ctx.json(user); }) ``` ```bash{1} [client] > GET /users/13 HTTP/1.1 -> Host: localhost:5000 -> User-Agent: curl/7.81.0 +> Host: localhost:8080 +> User-Agent: curl/8.5.0 > Accept: */* - +> < HTTP/1.1 200 OK -< X-Powered-By: Express -< Content-Type: application/json; charset=utf-8 -< Content-Length: 46 -< ETag: W/"2e-kXyTSzYpI5ic6GsVpLyJPBxDD0E" -< Date: Thu, 22 Feb 2024 16:50:22 GMT -< Connection: keep-alive -< Keep-Alive: timeout=5 - +< Date: Thu, 18 Jul 2024 10:52:45 GMT +< Content-Type: application/json +< Content-Length: 212 +< { "id": 13, "username": "EdShearing", From 16a2aa23e3be026c54396995b77bac3dde17bf15 Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Thu, 18 Jul 2024 14:50:00 +0000 Subject: [PATCH 09/13] Add body and headers for jackson --- .cbfmt.toml | 3 +++ src/javalin/body-and-headers.md | 48 +++++++++++++++++++++------------ 2 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 .cbfmt.toml diff --git a/.cbfmt.toml b/.cbfmt.toml new file mode 100644 index 0000000..47ec74c --- /dev/null +++ b/.cbfmt.toml @@ -0,0 +1,3 @@ +[languages] +js = ["prettier"] +java = ["google-java-format -"] diff --git a/src/javalin/body-and-headers.md b/src/javalin/body-and-headers.md index 16cec95..a1d4036 100644 --- a/src/javalin/body-and-headers.md +++ b/src/javalin/body-and-headers.md @@ -1,44 +1,52 @@ # Body and headers - - ## Endpoints with body -When we send a request, the headers are available in the backend as -`req.headers`. +When we send a request, the headers are available in the backend as `ctx.header()`. -When we send a request with a body, it will be parsed into the `req.body` for us -on the backend. +When we send a request with a JSON body, we can use `ctx.bodyAsClass(Some.class)` to parse the JSON into an object using a class. ::: code-group -```js{1} [server] -app.post('/users', async (req, res) => { - const user = await User.create(req.body.username, req.body.verified) - res.json(user) -}) +```java{8-10,13-18} [server] +public class App { + private Javalin app; + + public App() { + app.post( + "/users", + ctx -> { + var body = ctx.bodyAsClass(UserRequestBody.class); + var newUser = UserRepository.create(body.username, body.verified); + res.status(HttpStatus.CREATED).json(newUser); + }); + } + + class UserRequestBody { + public String username; + public boolean verified; + + public UserRequestBody() {} + } +} ``` ```bash{3} [client] -curl -v -X POST http://localhost:5000/users \ +curl -v -X POST http://localhost:8080/users \ -H "Content-Type: application/json" \ -d '{"username": "MinnieMouse", "verified": true}' > POST /users HTTP/1.1 -> Host: localhost:5000 +> Host: localhost:8080 > User-Agent: curl/7.81.0 > Accept: */* > Content-Type: application/json > Content-Length: 45 < HTTP/1.1 200 OK -< X-Powered-By: Express < Content-Type: application/json; charset=utf-8 < Content-Length: 47 -< ETag: W/"2f-nb/7y2Be3oCM0RJlX39MzZ6dYkE" < Date: Thu, 22 Feb 2024 16:57:57 GMT -< Connection: keep-alive -< Keep-Alive: timeout=5 { "id": 21, @@ -52,6 +60,12 @@ curl -v -X POST http://localhost:5000/users \ ::: tip +Since the `ctx.bodyAsClass()` method uses the Jackson ObjectMapper under the hood, it needs a default constructor. It will then fill in the fields that match betwwen the class and the JSON body. + +::: + +::: tip + According to the [HTTP standard](https://www.rfc-editor.org/rfc/rfc9110.html#name-terminology-and-core-concep), `GET` requests cannot have a body, so we generally only use body in requests From 9b66ac3afab74f72578e73ac89b7a5b18880e73c Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Fri, 19 Jul 2024 08:48:04 +0000 Subject: [PATCH 10/13] Finish D3E1 --- src/javalin/body-and-headers.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/javalin/body-and-headers.md b/src/javalin/body-and-headers.md index a1d4036..30dd0d6 100644 --- a/src/javalin/body-and-headers.md +++ b/src/javalin/body-and-headers.md @@ -2,7 +2,7 @@ ## Endpoints with body -When we send a request, the headers are available in the backend as `ctx.header()`. +When we send a request, the headers are available in the backend as `ctx.header(String)`. When we send a request with a JSON body, we can use `ctx.bodyAsClass(Some.class)` to parse the JSON into an object using a class. @@ -58,11 +58,7 @@ curl -v -X POST http://localhost:8080/users \ ::: -::: tip - -Since the `ctx.bodyAsClass()` method uses the Jackson ObjectMapper under the hood, it needs a default constructor. It will then fill in the fields that match betwwen the class and the JSON body. - -::: +Since the `ctx.bodyAsClass()` method uses the Jackson ObjectMapper under the hood, it needs a default constructor. It will then fill in the public fields that match betwwen the class and the JSON body. ::: tip From a1c93e7cffb87c8bcb29ffd0b9bb2eaa23d19889 Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Fri, 26 Jul 2024 15:03:15 +0000 Subject: [PATCH 11/13] wip --- .vitepress/sidebars/javalin.js | 3 +- src/javalin/body-and-headers.md | 10 ++- src/javalin/request-response.md | 2 +- src/javalin/routing.md | 111 +------------------------------ src/javalin/schema-validation.md | 46 ------------- src/javalin/sending-errors.md | 92 +++++++++++++++---------- 6 files changed, 69 insertions(+), 195 deletions(-) delete mode 100644 src/javalin/schema-validation.md diff --git a/.vitepress/sidebars/javalin.js b/.vitepress/sidebars/javalin.js index 8713934..53bbba5 100644 --- a/.vitepress/sidebars/javalin.js +++ b/.vitepress/sidebars/javalin.js @@ -21,6 +21,7 @@ export const javalin = [ text: 'Creating an API', items: [ { text: 'Creating a server', link: '/javalin/creating-a-server' }, + { text: 'Routing and Controllers', link: '/javalin/routing' }, { text: 'Request and response', link: '/javalin/request-response', @@ -31,9 +32,7 @@ export const javalin = [ { text: 'Body and headers', link: '/javalin/body-and-headers' } ] }, - { text: 'Routing', link: '/javalin/routing' }, { text: 'Sending errors', link: '/javalin/sending-errors' }, - { text: 'Schema validation', link: '/javalin/schema-validation' } ] }, { diff --git a/src/javalin/body-and-headers.md b/src/javalin/body-and-headers.md index 30dd0d6..b77d957 100644 --- a/src/javalin/body-and-headers.md +++ b/src/javalin/body-and-headers.md @@ -1,9 +1,17 @@ # Body and headers -## Endpoints with body +## Headers When we send a request, the headers are available in the backend as `ctx.header(String)`. +## Endpoints with body + +### Accessing the body text + +### Form data + +### JSON + When we send a request with a JSON body, we can use `ctx.bodyAsClass(Some.class)` to parse the JSON into an object using a class. ::: code-group diff --git a/src/javalin/request-response.md b/src/javalin/request-response.md index f9fd1a7..8dbbfbd 100644 --- a/src/javalin/request-response.md +++ b/src/javalin/request-response.md @@ -21,7 +21,7 @@ these requests in production. ## Sending and receiving JSON -We can send and receive JSON. +Sending JSON is relatively simple if we're happy with exposing the public fields and getters. ::: code-group diff --git a/src/javalin/routing.md b/src/javalin/routing.md index f52b0ba..6597e2c 100644 --- a/src/javalin/routing.md +++ b/src/javalin/routing.md @@ -1,110 +1,3 @@ -# Routing +# Routing and Controllers -## Creating a router - -To create an express router, we import the `Router` function from express. - -```js -import { Router } from 'express' -const router = Router() -``` - -## Adding endpoints - -We can add endpoints to `router` just like we do to `app`, by using the -`.get()`, `.post()`, `.put()` and `.delete()` methods. - -```js -router.get('/:userId', async (req, res) => { - const { userId } = req.params - const user = await User.findById(userId) - res.json(user) -}) -``` - -## Attaching the router - -We attach our router to `app` using `app.use()`. - -```js -app.use('/users', userRouter) -``` - -Notice that the `/users` prefix is added to all routes in the `userRouter`, -resulting in `/users/:userId` and so on. - -## Multiple routers - -We usually use routers to split our API routes into separate files, making the -project easier to maintain. - -::: code-group - -```js [users.js] -import { Router } from 'express' -import User from '../models/User.js' - -const router = Router() - -router.get('/', async (req, res) => { - const { limit } = req.query - const users = await User.findAll(limit) - res.json(users) -}) - -router.get('/:userId', async (req, res) => { - const { userId } = req.params - const user = await User.findById(userId) - res.json(user) -}) - -router.post('/', async (req, res) => { - const { username, verified } = req.body - const user = await User.create(username, verified) - res.json(user) -}) - -export default router -``` - -```js [bleets.js] -import { Router } from 'express' -import Bleet from '../models/Bleet.js' - -const router = Router() - -router.get('/', async (req, res) => { - const bleets = await Bleet.findAll() - res.json(bleets) -}) - -router.get('/:bleetId', async (req, res) => { - const { bleetId } = req.params - const bleet = await Bleet.findById(bleetId) - res.json(bleet) -}) - -router.post('/', async (req, res) => { - const { content, userId } = req.body - const bleet = await Bleet.create(content, userId) - res.json(bleet) -}) - -export default router -``` - -```js [app.js] -import express from 'express' -import userRouter from '../routers/users.js' -import bleetRouter from '../routers/bleets.js' - -const app = express() -app.use(express.json()) - -app.use('/users', userRouter) -app.use('/bleets', bleetRouter) - -export default app -``` - -::: +## qq diff --git a/src/javalin/schema-validation.md b/src/javalin/schema-validation.md deleted file mode 100644 index ae61722..0000000 --- a/src/javalin/schema-validation.md +++ /dev/null @@ -1,46 +0,0 @@ -# Schema validation - - - -## Validation libraries - -When a client sends us a large object, checking each property one by one is -tedious. There are libraries such as Yup and Joi to help with this. We'll be -using Zod. - -## Creating a schema - -We can create a schema using Zod. - -```js -import { z } from 'zod' - -const UserSchema = z.object({ - username: z.string(), - firstName: z.string(), - lastName: z.string(), - email: z.string().email(), - avatar: z.string().optional(), - password: z.string() -}) -``` - -## Validating data - -When a user sends us some data (which we will call the `payload`) we need to -check that it matches the schema, i.e. that it has the correct properties and -that they are of the correct type. - -```js -const valid = UserSchema.safeParse(payload).success - -if (!valid) { - throw new AppError('User data is not valid.', 400) -} - -// if the error is not thrown, we can be confident that -// the payload has the correct properties -``` - -Do check out the [Zod docs](https://zod.dev/) to find out more about what it can -do. diff --git a/src/javalin/sending-errors.md b/src/javalin/sending-errors.md index 76d05b1..edca46b 100644 --- a/src/javalin/sending-errors.md +++ b/src/javalin/sending-errors.md @@ -1,11 +1,8 @@ # Sending errors - - ## Error codes -When a client makes an HTTP request to the server, the server usually responds -with a status code. +When a client makes an HTTP request to the server, the server usually responds with a status code. Here are some of the more common status codes. @@ -25,56 +22,79 @@ Detailed information on all status codes can be found on In particular, we are interested in the error codes, (`400`-`599`). -## Custom error class +## Error Responses -To allow us to throw errors with status codes, we can extend the `Error` class. +Javalin includes some `HttpResponseException`s that we can use to trigger error responses. -```js -class AppError extends Error { - constructor(message, code) { - super(message) - this.code = code +```java +public class HomeController { + public static void post() throws HttpResponseException { + throw new ForbiddenResponse(); + } +} + +public class App { + private Javalin app; + + public App() { + app = Javalin.create(); + app.post("/", HomeController::post); } } ``` -## Throwing custom errors +::: tip -In our models, we can throw errors that arise from handling data, which is where -most errors occur. +A list of available error responses are available on [Javalin's documentation](https://javalin.io/documentation#default-responses). They are all named consistently in the form `statusName + "Response"`. -```js{2-4,9-11} -async findById(id) { - if (isNaN(id)) { - throw new AppError('ID must be a number.', 400) - } +::: - const query = 'SELECT * FROM users WHERE id = ?' - const results = await db.raw(query, [id]) +## Custom exceptions - if (!results.length) { - throw new AppError(`User with id ${id} does exist.`, 404) - } +## Throwing errors - return results[0] +In our models, we can throw errors that arise from handling data, which is where most errors occur. + +```java +public class UserRepository { + public static User findById(int id) throws HttpResponseException { + if (id < 0) { + throw new BadRequestResponse("ID is invalid"); + } + + var query = "SELECT id, username, firstName, lastName, email, avatar FROM users WHERE id = ?"; + + try (var con = DB.getConnection(); + var stmt = con.prepareStatement(query)) { + stmt.setInt(1, id); + try (var rs = stmt.executeQuery()) { + if (!rs.next()) { + throw new NotFoundResponse(""); + } + var username = rs.getString("username"); + var email = rs.getString("email"); + var verified = rs.getBoolean("avatar"); + + return new User(id, username, email, verified); + } + } + } } ``` ## Catching errors -We can catch the error in our controller and decide what to do with it. In this -case, we pass the error to an error handler. +We can catch the error in our controller and decide what to do with it. In this case, we pass the error to an error handler. -```js -router.get('/:userId', async (req, res, next) => { - const { userId } = req.params - try { - const user = await User.findById(userId) - res.json(user) - } catch (err) { - next(err) +```java +public class App { + private Javalin app; + + public App() { + app = Javalin.create(); + app.get("/user/{userId}", ctx -> {}); } -}) +} ``` ## Handling errors From 93201474c2c19c0d98f6bd49bca22995e0307cc6 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 30 Jul 2024 14:31:21 +0100 Subject: [PATCH 12/13] add static files --- src/javalin/static-files.md | 46 ++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/javalin/static-files.md b/src/javalin/static-files.md index f0724bd..746cca3 100644 --- a/src/javalin/static-files.md +++ b/src/javalin/static-files.md @@ -1,7 +1,5 @@ # Static files - - ## Serving static files Until now, we have been using our server to send back data such as strings or @@ -10,37 +8,33 @@ json. However, we can serve whole files such as pictures or documents, too! ## Public directory Although you can put the files you'd like to serve anywhere, it's convention to -put them in a folder called `public` +put them in the `resources` directory under a folder called `public` ``` -public -├── logo.png -└── logs - └── hello.txt +src +└── main + └── resources + └── public + ├── logs + │   └── hello.txt + └── logo.png ``` -## Using `express.static()` - -We instruct our express app to serve files from `public` using -`express.static()`. - -```js{11-12} -import express from 'express' -import api from '../api/index.js' -import { errorHandler } from './errors.js' - -const app = express() - -// configure api -app.use(express.json()) -app.use('/api', api) +## Using `config.staticFiles.add()` -// serve static files -app.use(express.static('public')) +We instruct our javalin app to serve files from `public` using `config.staticFiles.add()`. -app.use(errorHandler) -export default app +```java{6-9} +public class App { + public Javalin app; + public App() { + app = Javalin.create( + config -> { + config.staticFiles.add("/public", Location.CLASSPATH); + }); + } +} ``` ## Requesting static files From c262c12456441ad1eedf6bdc11b4463f1abb035e Mon Sep 17 00:00:00 2001 From: Andy Huang Date: Wed, 7 Aug 2024 10:44:23 +0000 Subject: [PATCH 13/13] wip creating maven --- .vitepress/sidebars/java.js | 76 ++++++++++++++++++++++++++++++++ src/java/maven.md | 4 ++ src/javalin/template-partials.md | 44 +++++++++++------- 3 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 src/java/maven.md diff --git a/.vitepress/sidebars/java.js b/.vitepress/sidebars/java.js index 81e0550..32d5ab3 100644 --- a/.vitepress/sidebars/java.js +++ b/.vitepress/sidebars/java.js @@ -18,5 +18,81 @@ export const java = [ }, { text: 'Data types', link: '/java/data-types' } ] + }, + { + text: 'Data and iteration', + collapsed: true, + items: [ + { + text: 'Arrays', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] + }, + { + text: 'Strings', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] + }, + { + text: 'Loops', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] + }, + + { + text: 'Objects', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] + } + ] + }, + + { + text: 'Object oriented programming', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' }, + { + text: 'Design patterns', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] + } + ] + }, + + { + text: 'Testing', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] + }, + + { + text: 'Building projects', + collapsed: true, + items: [ + { text: 'Creating a Maven project', link: '/java/qq' }, + { text: 'Making a CLI', link: '/java/qq' }, + { text: 'Handling Errors', link: '/java/qq' }, + ] + }, + + { + text: 'Asynchronous code', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] } ] diff --git a/src/java/maven.md b/src/java/maven.md new file mode 100644 index 0000000..53504f2 --- /dev/null +++ b/src/java/maven.md @@ -0,0 +1,4 @@ +# Creating a Maven project + +## + diff --git a/src/javalin/template-partials.md b/src/javalin/template-partials.md index 87def5a..3d874be 100644 --- a/src/javalin/template-partials.md +++ b/src/javalin/template-partials.md @@ -1,7 +1,5 @@ # Template partials - - ## The problem Many pages in a website might share the same content. For example, our views @@ -9,13 +7,16 @@ might look like this (the repeated content has been highlighted): ::: code-group -```html{1-9,12-13} [index.ejs] +```html{1-9,12-13} [index.jte] +@import bleeter.models.Page +@param Page page + - Bleeter | <%= title %> + Bleeter | ${page.title} @@ -25,43 +26,54 @@ might look like this (the repeated content has been highlighted): ``` -```html{1-9,20-21} [bleets.ejs] +```html{1-9,20-21} [bleets.jte] +@import java.util.List +@import bleeter.models.Page +@import bleeter.models.Bleet +@param Page page +@param List bleets + - Bleeter | <%= title %> + Bleeter | ${page.title}

      Bleets

        - <% for (let bleet of bleets) { %> + @for(var bleet : bleets)
      1. - <%= bleet.content.slice(0, 10) + '...' %> - Read more + ${bleet.content.substring(0, 10)}... + Read more
      2. - <% } %> + @endfor
      ``` -```html{1-9,13-14} [bleet.ejs] +```html{1-9,13-14} [bleet.jte] +@import bleeter.models.Page +@import bleeter.models.Bleet +@param Page page +@param Bleet bleet + - Bleeter | <%= title %> + Bleeter | ${page.title}

      A bleet

      -

      <%= bleet.content %>

      -

      <%= bleet.createdAt %>

      +

      ${bleet.content}

      +

      ${bleet.createdAt}

      ``` @@ -72,14 +84,14 @@ This is a problem, because if we need to make a change to the shared parts of each view, we need to update each view individually, which is extra work and an opportunity for mistakes. -## Creating partials +## Template calls We can create partial templates and use them in our views. The directory structure might look like this: ```txt -views/ +templates/ ├── bleet.ejs ├── bleets.ejs ├── index.ejs