Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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].sort((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