Skip to content

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:

bash
mkdir my-remix-app && cd my-remix-app
npm init -y
npm install remix@next
npm install -D tsx @types/node

Here 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 a package.json file, 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: tsx to run TypeScript directly, and @types/node to give TypeScript knowledge of Node.js built-in APIs.

2. Create tsconfig.json

Create a file called tsconfig.json in the project root:

json
{
  "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:

ts
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:

ts
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:http is Node.js's built-in HTTP server module.
  • createRequestListener bridges Remix's modern Request/Response API with Node.js's older server API.
  • createRouter creates a router -- the object that decides which code runs for a given URL.
  • route defines your URL patterns.

Defining routes:

ts
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:

ts
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:

ts
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:

ts
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:

json
{
  "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:

bash
npm run dev

You should see:

Server running at http://localhost:3000

6. Try It Out

Open your browser and visit these URLs:

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:

ts
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.

Released under the MIT License.