-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1774 from fireship-io/deno
content: deno course
- Loading branch information
Showing
40 changed files
with
1,871 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
--- | ||
lastmod: 2024-11-07T11:11:30-09:00 | ||
title: Deno Full Course | ||
description: Master TypeScript and Backend WebDev with Deno | ||
weight: 0 | ||
type: courses | ||
vimeo: 1027549123 | ||
author: Jeff Delaney | ||
tags: | ||
- typescript | ||
- deno | ||
- pro | ||
|
||
stack: | ||
- deno | ||
- ts | ||
- js | ||
--- | ||
|
||
**Deno - The Full Course** is a hands-on tutorial where you will build a complete web app with [Deno](https://kit.svelte.dev/), using zero 3rd-party denpendices to master TypeScript and Web Platform APIs. | ||
|
||
## What will I learn? | ||
|
||
- 🦕 Everything you need to be productive with Deno | ||
- 💪 TypeScript fundamentals in fast to-the-point videos | ||
- 🕸️ Web Platform APIs that work everywhere | ||
- 💥 Build a full-stack web app with zero-dependencies | ||
- 🧪 Test-Driven Development & Benchmarking | ||
- 🍪 Roll your own cookie-based user authentication | ||
- 🎹 Manage data with Deno KV | ||
- ⚡ Stream database changes in realtime | ||
- ⚛️ Server-rendered HTML with JSX | ||
- 🚀 Deno Deployment | ||
|
||
|
||
## 🦄 What will I build? | ||
|
||
You will build a **Realtime Link Shortener** inspired by [🌴 Bit.ly](https://bit.ly/) where users can create sharable links and. The goal of this project is to help you master web development fundamentals and learn advanced TypeScript patterns. | ||
|
||
### 🚀 Try it out! | ||
|
||
Visit the demo app and give it a test drive before you enroll. | ||
|
||
<div> | ||
<a href="https://link.fireship.app" class="btn btn-orange">Link Shortener Live Demo</a> | ||
</div> | ||
|
||
## 🤔 Is this Course Right for Me? | ||
|
||
<div class="box box-blue"> | ||
This course is intermediate level 🟦 and expects some familiarity with JavaScript and web development. The content is fast-paced and similar to my style on YouTube, but far more in-depth and should be followed in a linear format. | ||
</div> | ||
|
||
|
||
## When was the course last updated? | ||
|
||
<span class="tag tag-sm tag-pro">Updated November 7th, 2024</span> <span class="tag tag-sm tag-deno">Deno 2</span> | ||
|
||
## How do I enroll? | ||
|
||
The first few videos are *free*, so just give it try. When you reach a paid module, you will be asked to pay for a single course or upgrade to PRO. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
--- | ||
title: Atomic Writes | ||
description: ACID compliant transactions | ||
weight: 49 | ||
lastmod: 2024-11-05T11:11:30-09:00 | ||
draft: false | ||
vimeo: 1027526766 | ||
emoji: 💥 | ||
video_length: 3:25 | ||
--- | ||
|
||
|
||
|
||
## Query Records by Username | ||
|
||
|
||
{{< file "ts" "main.ts" >}} | ||
```typescript | ||
export async function storeShortLink( | ||
longUrl: string, | ||
shortCode: string, | ||
userId: string, | ||
) { | ||
const shortLinkKey = ["shortlinks", shortCode]; | ||
const data: ShortLink = { | ||
shortCode, | ||
longUrl, | ||
userId, | ||
createdAt: Date.now(), | ||
clickCount: 0, | ||
}; | ||
|
||
const userKey = [userId, shortCode]; | ||
|
||
const res = await kv.atomic() | ||
.set(shortLinkKey, data) | ||
.set(userKey, shortCode) | ||
.commit() | ||
|
||
|
||
return res; | ||
} | ||
|
||
export async function getUserLinks(userId: string) { | ||
|
||
const list = kv.list<string>({ prefix: [userId]}); | ||
const res = await Array.fromAsync(list); | ||
const userShortLinkKeys = res.map((v) => ['shortlinks', v.value]); | ||
|
||
const userRes = await kv.getMany<ShortLink[]>(userShortLinkKeys) | ||
const userShortLinks = await Array.fromAsync(userRes) | ||
|
||
return userShortLinks.map(v => v.value); | ||
} | ||
``` | ||
|
||
## Increment a Count | ||
|
||
{{< file "ts" "main.ts" >}} | ||
```typescript | ||
export async function incrementClickCount( | ||
shortCode: string, | ||
data?: Partial<ClickAnalytics>, | ||
) { | ||
const shortLinkKey = ["shortlinks", shortCode]; | ||
const shortLink = await kv.get(shortLinkKey); | ||
const shortLinkData = shortLink.value as ShortLink; | ||
|
||
const newClickCount = shortLinkData?.clickCount + 1; | ||
|
||
const analyicsKey = ["analytics", shortCode, newClickCount]; | ||
const analyticsData = { | ||
shortCode, | ||
createdAt: Date.now(), | ||
...data, | ||
// ipAddress: "192.168.1.1", | ||
// userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", | ||
// country: "United States" | ||
}; | ||
|
||
const res = await kv.atomic() | ||
.check(shortLink) | ||
.set(shortLinkKey, { | ||
...shortLinkData, | ||
clickCount: shortLinkData?.clickCount + 1, | ||
}) | ||
.set(analyicsKey, analyticsData) | ||
.commit(); | ||
if (res.ok) { | ||
console.log("Logged click"); | ||
} else { | ||
console.error("Not logged"); | ||
} | ||
|
||
return res; | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
--- | ||
title: User Auth | ||
description: Roll your own user authentication flow | ||
weight: 47 | ||
lastmod: 2024-11-05T11:11:30-09:00 | ||
draft: false | ||
vimeo: 1027526785 | ||
emoji: 🫂 | ||
video_length: 5:35 | ||
--- | ||
|
||
|
||
|
||
## Setup Environment Variables | ||
|
||
{{< file "cog" ".env" >}} | ||
```bash | ||
GITHUB_CLIENT_ID=abc | ||
GITHUB_CLIENT_SECRET=xyz | ||
REDIRECT_URI=http://localhost:8000/oauth/callback | ||
``` | ||
|
||
Update the dev task to include the `--env` flag to load the env vars: | ||
|
||
{{< file "deno" "deno.json" >}} | ||
```json | ||
{ | ||
"tasks": { | ||
"dev": "deno serve --watch --unstable-kv --env -A src/main.ts", | ||
}, | ||
} | ||
``` | ||
|
||
## Authentication with User Profile | ||
|
||
|
||
Add database logic to store and get the user profile from the Deno KV database. | ||
|
||
{{< file "ts" "db.ts" >}} | ||
```typescript | ||
export type GitHubUser = { | ||
login: string; // username | ||
avatar_url: string; | ||
html_url: string; | ||
}; | ||
|
||
|
||
export async function storeUser(sessionId: string, userData: GitHubUser) { | ||
const key = ["sessions", sessionId]; | ||
const res = await kv.set(key, userData); | ||
return res; | ||
} | ||
|
||
export async function getUser(sessionId: string) { | ||
const key = ["sessions", sessionId]; | ||
const res = await kv.get<GitHubUser>(key); | ||
return res.value; | ||
} | ||
``` | ||
|
||
Create a new file to handle authentication logic | ||
|
||
{{< file "ts" "auth.ts" >}} | ||
```typescript | ||
import { createGitHubOAuthConfig, createHelpers } from "jsr:@deno/kv-oauth"; | ||
import { pick } from "jsr:@std/collections/pick"; | ||
import { type GitHubUser, getUser, storeUser } from "./db.ts"; | ||
|
||
const oauthConfig = createGitHubOAuthConfig(); | ||
const { | ||
handleCallback, | ||
getSessionId, | ||
} = createHelpers(oauthConfig); | ||
|
||
|
||
export async function getCurrentUser(req: Request) { | ||
const sessionId = await getSessionId(req); | ||
console.log(sessionId) | ||
return sessionId ? await getUser(sessionId) : null; | ||
} | ||
|
||
export async function getGitHubProfile(accessToken: string) { | ||
const response = await fetch("https://api.github.com/user", { | ||
headers: { authorization: `Bearer ${accessToken}` }, | ||
}); | ||
|
||
if (!response.ok) { | ||
response.body?.cancel(); | ||
throw new Error("Failed to fetch GitHub user"); | ||
} | ||
|
||
return response.json() as Promise<GitHubUser>; | ||
} | ||
|
||
export async function handleGithubCallback(req: Request) { | ||
const { response, tokens, sessionId } = await handleCallback(req); | ||
const userData = await getGitHubProfile(tokens?.accessToken); | ||
const filteredData = pick(userData, ["avatar_url", "html_url", "login"]); | ||
await storeUser(sessionId, filteredData); | ||
return response; | ||
} | ||
|
||
``` | ||
|
||
Add the current user as a property on the router | ||
|
||
{{< file "ts" "router.ts" >}} | ||
```typescript | ||
import type { GitHubUser } from "./db.ts"; | ||
import { getCurrentUser } from "./auth.ts"; | ||
|
||
export class Router { | ||
#routes: Route[] = []; | ||
|
||
currentUser?: GitHubUser | null; // <-- HERE | ||
|
||
#addRoute(method: string, path: string, handler: Handler) { | ||
const pattern = new URLPattern({ pathname: path }); | ||
|
||
this.#routes.push({ | ||
pattern, | ||
method, | ||
handler: async (req, info, params) => { | ||
try { | ||
this.currentUser = await getCurrentUser(req); // <-- HERE | ||
return await handler(req, info!, params!); | ||
} catch (error) { | ||
console.error("Error handling request:", error); | ||
return new Response("Internal Server Error", { status: 500 }); | ||
} | ||
}, | ||
}); | ||
} | ||
|
||
get handler() { | ||
return route(this.#routes, () => new Response("Not Found", { status: 404 })) | ||
} | ||
|
||
} | ||
``` | ||
|
||
Configure the OAuth 2.0 routes | ||
|
||
{{< file "ts" "main.ts" >}} | ||
```typescript | ||
import { createGitHubOAuthConfig, createHelpers } from "jsr:@deno/kv-oauth"; | ||
import { handleGithubCallback } from "./auth.ts"; | ||
|
||
const app = new Router(); | ||
|
||
const oauthConfig = createGitHubOAuthConfig({ | ||
redirectUri: Deno.env.get('REDIRECT_URI') | ||
}); | ||
const { | ||
signIn, | ||
signOut, | ||
} = createHelpers(oauthConfig); | ||
|
||
|
||
app.get("/oauth/signin", (req: Request) => signIn(req)); | ||
app.get("/oauth/signout", signOut); | ||
app.get("/oauth/callback", handleGithubCallback); | ||
|
||
|
||
app.get("/", () => { | ||
return new Response( | ||
render(HomePage({ user: app.currentUser })), | ||
{ | ||
status: 200, | ||
headers: { | ||
"content-type": "text/html", | ||
}, | ||
}); | ||
}); | ||
``` |
Oops, something went wrong.