Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/fetch-router/demos/node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# fetch-router Node Example

This example is a [Node.js server](https://nodejs.org/) that handles routing using `@remix-run/fetch-router`
44 changes: 44 additions & 0 deletions packages/fetch-router/demos/node/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export interface Post {
id: string
title: string
content: string
author: string
createdAt: Date
}

let posts: Post[] = [
{
id: '1',
title: 'Welcome to the Blog',
content: 'This is a simple blog demo built with fetch-router on Node.js.',
author: 'Admin',
createdAt: new Date('2025-01-01'),
},
{
id: '2',
title: 'Getting Started with fetch-router',
content: 'fetch-router is a minimal, composable router built on the web Fetch API.',
author: 'Admin',
createdAt: new Date('2025-01-02'),
},
]

export function getPosts() {
return posts.toSorted((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
}

export function getPost(id: string) {
return posts.find((p) => p.id === id)
}

export function createPost(title: string, content: string, author: string) {
let post: Post = {
id: String(posts.length + 1),
title,
content,
author,
createdAt: new Date(),
}
posts.push(post)
return post
}
20 changes: 20 additions & 0 deletions packages/fetch-router/demos/node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "fetch-router-node-demo",
"private": true,
"type": "module",
"dependencies": {
"@remix-run/cookie": "workspace:*",
"@remix-run/fetch-router": "workspace:*",
"@remix-run/html-template": "workspace:*",
"@remix-run/node-fetch-server": "workspace:*",
"@remix-run/session": "workspace:*"
},
"devDependencies": {
"@types/node": "^24.6.0"
},
"scripts": {
"dev": "node --watch server.ts",
"start": "node server.ts",
"typecheck": "tsc --noEmit"
}
}
195 changes: 195 additions & 0 deletions packages/fetch-router/demos/node/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { createRouter } from '@remix-run/fetch-router'
import { createCookie } from '@remix-run/cookie'
import { createCookieStorage } from '@remix-run/session/cookie-storage'
import { formData } from '@remix-run/fetch-router/form-data-middleware'
import { logger } from '@remix-run/fetch-router/logger-middleware'
import { session } from '@remix-run/fetch-router/session-middleware'
import { html } from '@remix-run/html-template'
import * as res from '@remix-run/fetch-router/response-helpers'
import type { Middleware } from '@remix-run/fetch-router'

import { routes } from './routes.ts'
import * as data from './data.ts'

let sessionCookie = createCookie('__sess', {
secrets: ['s3cr3t'],
})
let storage = createCookieStorage()

function requireAuth(): Middleware {
return async ({ session }, next) => {
let username = session.get('username')
if (!username) {
return res.redirect(routes.login.index.href())
}
return next()
}
}

export let router = createRouter({
middleware: [logger(), formData(), session(sessionCookie, storage)],
})

router.map(routes.home, ({ session }) => {
let posts = data.getPosts()
let username = session.get('username') as string | undefined

return res.html(html`
<!doctype html>
<html>
<head>
<title>Simple Blog - fetch-router Demo</title>
<meta charset="utf-8" />
</head>
<body>
<nav>
<h1>Simple Blog</h1>
<div>
${username
? html`
<span>Hello, ${username}!</span>
<form
method="POST"
action="${routes.logout.href()}"
style="display: inline; margin-left: 10px;"
>
<button type="submit">Logout</button>
</form>
<a href="${routes.posts.new.href()}" style="margin-left: 10px;">New Post</a>
`
: html`<a href="${routes.login.index.href()}">Login</a>`}
</div>
</nav>
<main>
${posts.length === 0 ? html`<p>No posts yet.</p>` : null}
${posts.map(
(post) => html`
<article>
<h2><a href="${routes.posts.show.href({ id: post.id })}">${post.title}</a></h2>
<p>${post.content.substring(0, 150)}${post.content.length > 150 ? '...' : ''}</p>
<div>By ${post.author} on ${post.createdAt.toLocaleDateString()}</div>
</article>
`,
)}
</main>
</body>
</html>
`)
})

router.map(routes.login, {
index({ session }) {
let username = session.get('username') as string | undefined
if (username) {
return res.redirect(routes.home.href())
}

return res.html(html`
<!doctype html>
<html>
<head>
<title>Login - Simple Blog</title>
<meta charset="utf-8" />
</head>
<body>
<h1>Login</h1>
<p>Enter any username to login (no password required for demo)</p>
<form method="POST" action="${routes.login.action.href()}">
<div style="display: flex; flex-direction: column; gap: 10px; width: 150px;">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required />
<label for="password">Password:</label>
<input type="password" id="password" name="password" required />
</div>
<br />
<button type="submit">Login</button>
</form>
<p><a href="${routes.home.href()}">← Back to Home</a></p>
</body>
</html>
`)
},
async action({ formData, session }) {
let username = formData.get('username') as string
if (!username) {
return res.redirect(routes.login.index.href())
}
session.set('username', username)
return res.redirect(routes.home.href())
},
})

router.post(routes.logout, ({ session }) => {
session.destroy()
return res.redirect(routes.home.href())
})

router.map(routes.posts, {
new: {
middleware: [requireAuth()],
handler({ session: _session }) {
return res.html(html`
<!doctype html>
<html>
<head>
<title>New Post - Simple Blog</title>
<meta charset="utf-8" />
</head>
<body>
<h1>New Post</h1>
<form method="POST" action="${routes.posts.create.href()}">
<div>
<label for="title">Title:</label>
<input type="text" id="title" name="title" required />
</div>
<div>
<label for="content">Content:</label>
<textarea id="content" name="content" required></textarea>
</div>
<button type="submit">Create Post</button>
</form>
<p><a href="${routes.home.href()}">← Back to Home</a></p>
</body>
</html>
`)
},
},
async create({ formData, session }) {
let username = session.get('username') as string
if (!username) {
return res.redirect(routes.login.index.href())
}

let title = formData.get('title') as string
let content = formData.get('content') as string

if (!title || !content) {
return res.redirect(routes.posts.new.href())
}

let post = data.createPost(title, content, username)
return res.redirect(routes.posts.show.href({ id: post.id }))
},
show({ params }) {
let post = data.getPost(params.id)
if (!post) {
return new Response('Post not found', { status: 404 })
}

return res.html(html`
<!doctype html>
<html>
<head>
<title>${post.title} - Simple Blog</title>
<meta charset="utf-8" />
</head>
<body>
<h1>${post.title}</h1>
<div>By ${post.author} on ${post.createdAt.toLocaleDateString()}</div>
<div>${post.content.replace(/\n/g, '<br>')}</div>
<p><a href="${routes.home.href()}">← Back to Home</a></p>
</body>
</html>
`)
},
})
8 changes: 8 additions & 0 deletions packages/fetch-router/demos/node/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { formAction, route, resources } from '@remix-run/fetch-router'

export let routes = route({
home: '/',
login: formAction('/login'),
logout: { method: 'POST', pattern: '/logout' },
posts: resources('posts', { only: ['new', 'create', 'show'] }),
})
21 changes: 21 additions & 0 deletions packages/fetch-router/demos/node/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as http from 'node:http'
import { createRequestListener } from '@remix-run/node-fetch-server'

import { router } from './router.ts'

const PORT = 3000

let server = http.createServer(
createRequestListener(async (request) => {
try {
return await router.fetch(request)
} catch (error) {
console.error(error)
return new Response('Internal Server Error', { status: 500 })
}
}),
)

server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`)
})
14 changes: 14 additions & 0 deletions packages/fetch-router/demos/node/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"include": ["server.ts", "../../global.d.ts"],
"compilerOptions": {
"strict": true,
"lib": ["ES2024", "DOM", "DOM.Iterable"],
"module": "ES2022",
"moduleResolution": "Bundler",
"target": "ESNext",
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
}
}
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ shellEmulator: true
packages:
- demos/*
- packages/*
- packages/fetch-router/demos/*
- packages/form-data-parser/demos/*
- packages/interaction/demos
- packages/lazy-file/scripts
Expand Down