Quick Start
In this guide you will build a working Remix V3 application from scratch. By the end, you will have a server that handles multiple routes and responds with HTML.
1. Create a New Project
Open your terminal and run the following commands to create a project directory and install dependencies:
mkdir my-remix-app && cd my-remix-app
npm init -y
npm install remix@next
npm install -D tsx @types/nodeHere is what each command does:
mkdir my-remix-app && cd my-remix-app-- Creates a new folder and moves into it.npm init -y-- Generates apackage.jsonfile, which tracks your project's dependencies and scripts.npm install remix@next-- Installs Remix V3 (the preview release).npm install -D tsx @types/node-- Installs development tools:tsxto run TypeScript directly, and@types/nodeto give TypeScript knowledge of Node.js built-in APIs.
2. Create tsconfig.json
Create a file called tsconfig.json in the project root:
{
"compilerOptions": {
"strict": true,
"lib": ["ES2024", "DOM", "DOM.Iterable"],
"module": "ES2022",
"moduleResolution": "Bundler",
"target": "ESNext",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true
}
}This configures TypeScript for Remix. See the Installation page for a full explanation of each setting.
3. Create server.ts
This is the heart of your application. Create a file called server.ts in the project root:
import * as http from 'node:http'
import { createRequestListener } from 'remix/node-fetch-server'
import { createRouter } from 'remix/fetch-router'
import { route } from 'remix/fetch-router/routes'
let routes = route({
home: '/',
about: '/about',
greet: '/hello/:name',
})
let router = createRouter()
router.map(routes.home, () => {
return new Response('<h1>Welcome to Remix!</h1>', {
headers: { 'Content-Type': 'text/html' },
})
})
router.map(routes.about, () => {
return new Response('<h1>About</h1><p>Built with Remix V3.</p>', {
headers: { 'Content-Type': 'text/html' },
})
})
router.map(routes.greet, ({ params }) => {
return new Response(`<h1>Hello, ${params.name}!</h1>`, {
headers: { 'Content-Type': 'text/html' },
})
})
let server = http.createServer(createRequestListener(router.fetch))
server.listen(3000, () => {
console.log('Server running at http://localhost:3000')
})What Each Part Does
Let's walk through the code:
Imports:
import * as http from 'node:http'
import { createRequestListener } from 'remix/node-fetch-server'
import { createRouter } from 'remix/fetch-router'
import { route } from 'remix/fetch-router/routes'node:httpis Node.js's built-in HTTP server module.createRequestListenerbridges Remix's modernRequest/ResponseAPI with Node.js's older server API.createRoutercreates a router -- the object that decides which code runs for a given URL.routedefines your URL patterns.
Defining routes:
let routes = route({
home: '/',
about: '/about',
greet: '/hello/:name',
})A route is a mapping between a URL pattern and a name. The :name in /hello/:name is a dynamic segment -- it matches any value in that position. For example, /hello/world matches with name set to "world".
Mapping handlers to routes:
router.map(routes.home, () => {
return new Response('<h1>Welcome to Remix!</h1>', {
headers: { 'Content-Type': 'text/html' },
})
})router.map connects a route to a handler function. A handler receives the incoming request and returns a Response. The Response object is a standard web API -- you set the body (the HTML string) and headers (metadata like Content-Type that tells the browser the response is HTML).
Accessing dynamic parameters:
router.map(routes.greet, ({ params }) => {
return new Response(`<h1>Hello, ${params.name}!</h1>`, {
headers: { 'Content-Type': 'text/html' },
})
})When a route has dynamic segments like :name, the matched values are available in params. Visiting /hello/world gives you params.name === "world".
Starting the server:
let server = http.createServer(createRequestListener(router.fetch))
server.listen(3000, () => {
console.log('Server running at http://localhost:3000')
})router.fetch is a function that takes a Request and returns a Response. createRequestListener wraps it so Node.js's HTTP server can use it. The server then listens on port 3000 for incoming connections.
4. Add a Dev Script
Open package.json and add a dev script inside the "scripts" section:
{
"scripts": {
"dev": "tsx watch server.ts"
}
}The tsx watch command runs your TypeScript file and automatically restarts whenever you save changes -- no need to stop and restart the server by hand.
5. Start the Server
Run the dev script:
npm run devYou should see:
Server running at http://localhost:30006. Try It Out
Open your browser and visit these URLs:
- http://localhost:3000 -- Shows "Welcome to Remix!"
- http://localhost:3000/about -- Shows the About page
- http://localhost:3000/hello/world -- Shows "Hello, world!" (try replacing
worldwith your own name)
You now have a working Remix V3 application.
Adding Middleware
As your application grows, you will want to add behavior that applies to every request -- logging, authentication, session handling, and so on. This is what middleware is for. Middleware is a function that runs before your route handler, adding capabilities or modifying the request.
Remix ships with built-in middleware you can use right away. For example, to add request logging:
import { logger } from 'remix/logger-middleware'
let router = createRouter({
middleware: [logger()],
})With this in place, every request will be logged to the console with the HTTP method, URL, status code, and response time. You can stack multiple middleware in the array, and they run in order.
TIP
Middleware is one of the most powerful patterns in Remix V3. You will learn how to write your own middleware in the Middleware concept guide.
Next Steps
Your app works, but everything is in a single file. As your application grows, you will want to split things into separate files and folders. The Project Structure guide shows you how to organize a Remix V3 project for real-world use.