1. Project Setup
In this first part of the tutorial, you will create a brand-new project, install Remix and its supporting tools, configure TypeScript, and start a development server that responds to requests in your browser.
By the end of this section, you will have a running web server that displays "Welcome to the Bookstore" when you visit http://localhost:3000.
Create the Project Directory
Open your terminal and create a new directory for the bookstore project:
mkdir bookstore
cd bookstoreInitialize it as a Node.js project. The npm init -y command creates a package.json file, which is the manifest that tracks your project's name, scripts, and dependencies:
npm init -yOpen the generated package.json and update it to use ES modules. ES modules are the modern JavaScript module system that uses import and export (as opposed to the older require() syntax). Add the "type": "module" field:
{
"name": "bookstore",
"private": true,
"type": "module",
"scripts": {
"dev": "NODE_ENV=development tsx watch server.ts"
}
}Let's break down what each field does:
"name"--- the name of your project. It can be anything."private": true--- prevents you from accidentally publishing this project to npm (the public package registry)."type": "module"--- tells Node.js to treat.jsfiles as ES modules, soimport/exportsyntax works."scripts"--- defines shortcut commands. The"dev"script runs your server in development mode. We will use it shortly.
What is package.json?
Every Node.js project has a package.json file at its root. It lists the packages (libraries) your project depends on, defines scripts you can run, and holds metadata like the project name and version. Think of it as a table of contents for your project.
Install Dependencies
Now install the packages your project needs. There are two kinds of dependencies:
- Dependencies --- packages your application needs to run (like Remix itself).
- Dev dependencies --- packages you only need during development (like TypeScript tools).
Install Remix:
npm install remix@nextInstall the development tools:
npm install -D tsx @types/node esbuildHere is what each package does:
| Package | Purpose |
|---|---|
remix@next | The Remix framework. The @next tag installs the V3 alpha. |
tsx | Runs TypeScript files directly, without a separate compile step. |
@types/node | TypeScript type definitions for Node.js built-in modules like http and fs. |
esbuild | A fast JavaScript bundler. You will use it later to bundle browser-side code. |
What does -D mean?
The -D flag (short for --save-dev) installs packages as dev dependencies. These are tools you need while building your app, but not when running it in production. In package.json, they appear under "devDependencies" instead of "dependencies".
Configure TypeScript
Create a tsconfig.json file in the root of your project. This file tells TypeScript how to check and compile your code:
{
"compilerOptions": {
"strict": true,
"lib": ["ES2024", "DOM", "DOM.Iterable"],
"module": "ES2022",
"moduleResolution": "Bundler",
"target": "ESNext",
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"jsxImportSource": "remix/component",
"preserveSymlinks": true
}
}Here is what the important settings do:
strict--- enables all of TypeScript's strictness checks. This catches more bugs but requires you to be more precise with types.lib--- tells TypeScript which built-in APIs are available.ES2024gives modern JavaScript features.DOMandDOM.Iterablegive web APIs likeRequest,Response, andHeaders.moduleandmoduleResolution--- configure howimportstatements are resolved. TheBundlerstrategy works with modern build tools.allowImportingTsExtensions--- lets you writeimport './foo.ts'with the.tsextension.jsxandjsxImportSource--- configure JSX to use Remix's built-in component system. JSX is a syntax that lets you write HTML-like code inside JavaScript --- you will learn more about it in Part 3.preserveSymlinks--- ensures that symlinked packages (common in monorepos) are resolved correctly.
What is TypeScript?
TypeScript is JavaScript with type annotations. Types describe the shape of your data --- for example, "this variable is a string" or "this function takes a number and returns a boolean." Your editor uses these types to give you autocomplete and catch mistakes before you run your code. You do not need to be a TypeScript expert; the types work quietly in the background.
Create the Server
Your Remix application is a web server --- a program that listens for HTTP requests from browsers and sends back responses. Let's create one.
Create a file called server.ts in the root of your project:
import * as http from 'node:http'
import { createRequestListener } from 'remix/node-fetch-server'
import { router } from './app/router.ts'
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 })
}
}),
)
let port = 3000
server.listen(port, () => {
console.log(`Bookstore is running on http://localhost:${port}`)
})Let's walk through every part of this file:
Line by Line
import * as http from 'node:http'This imports Node.js's built-in http module. The node: prefix is the standard way to import Node.js built-in modules. The http module gives you the ability to create a web server that listens for incoming requests.
import { createRequestListener } from 'remix/node-fetch-server'This imports a Remix helper that bridges two worlds. Node.js has its own way of handling HTTP requests (using req and res objects), but Remix uses the Fetch API --- the same Request and Response objects that browsers use. createRequestListener translates between the two, so you can write standard web code that runs on Node.js.
import { router } from './app/router.ts'This imports the router you will create next. The router is the core of your application --- it decides which code to run based on the URL of each incoming request.
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 })
}
}),
)This creates the HTTP server. For every incoming request:
createRequestListenerconverts the Node.js request into a standardRequestobject.- That
Requestis passed torouter.fetch(), which finds the right handler and runs it. - The handler returns a
Response, which gets sent back to the browser. - If anything goes wrong, the
catchblock logs the error and returns a generic "Internal Server Error" response.
let port = 3000
server.listen(port, () => {
console.log(`Bookstore is running on http://localhost:${port}`)
})This starts the server on port 3000. A port is a number that identifies a specific program on your computer. When you visit http://localhost:3000 in your browser, it connects to port 3000, where your bookstore server is listening.
Define the Routes
Routes tell your application which URLs it should respond to. Create a directory called app and a file called routes.ts inside it:
mkdir appimport { route } from 'remix/fetch-router/routes'
export const routes = route({
home: '/',
})This defines a single route: when someone visits / (the home page), the router will look for a handler mapped to routes.home. You will add many more routes in Part 2.
What is a route?
A route is a mapping between a URL pattern and the code that should run when someone visits that URL. For example, the route '/' matches the home page, /about would match the about page, and /books/:slug would match any URL like /books/my-book or /books/great-expectations. Routes are the backbone of any web application.
Create the Router
The router takes your route definitions and connects them to handlers --- the functions that actually process requests and return responses. Create app/router.ts:
import { createRouter } from 'remix/fetch-router'
import { compression } from 'remix/compression-middleware'
import { logger } from 'remix/logger-middleware'
import { staticFiles } from 'remix/static-middleware'
import { routes } from './routes.ts'
let middleware = []
if (process.env.NODE_ENV === 'development') {
middleware.push(logger())
}
middleware.push(compression())
middleware.push(staticFiles('./public'))
export let router = createRouter({ middleware })
router.map(routes.home, {
handler() {
return new Response(
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bookstore</title>
</head>
<body>
<h1>Welcome to the Bookstore</h1>
<p>Your bookstore is up and running.</p>
</body>
</html>`,
{
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
},
)
},
})Let's break this down:
createRouter
import { createRouter } from 'remix/fetch-router'createRouter creates a new router instance. The router is the central piece that receives every incoming request, checks it against your defined routes, runs any middleware, and calls the appropriate handler.
Middleware
let middleware = []
if (process.env.NODE_ENV === 'development') {
middleware.push(logger())
}
middleware.push(compression())
middleware.push(staticFiles('./public'))Middleware are small functions that run on every request, before your route handler. They can inspect or modify the request, add extra data to the response, or even respond directly. Think of them as a pipeline that each request flows through.
Here we use three middleware:
| Middleware | What it does |
|---|---|
logger() | Prints every request to the terminal (method, URL, status code, timing). Only enabled in development so your production logs stay clean. |
compression() | Compresses response bodies with gzip or brotli, making pages load faster. |
staticFiles('./public') | Serves files from the public directory (images, CSS, JavaScript) without needing a route for each one. |
Mapping Routes to Handlers
router.map(routes.home, {
handler() {
return new Response(/* ... */)
},
})router.map() connects a route to a handler. The handler is a function that receives the request and returns a Response. Here, we return a simple HTML page.
The Response constructor takes two arguments:
- The body --- the content to send back. Here it is an HTML string.
- An options object --- we set the
Content-Typeheader totext/htmlso the browser knows to render it as a web page (rather than displaying the raw text).
Create the Public Directory
The staticFiles middleware looks for files in a public directory. Create it now:
mkdir publicYou will put CSS files, images, and other static assets here later. For now, it just needs to exist so the middleware does not throw an error.
Run the Development Server
Everything is in place. Start the server:
npm run devYou should see output like this:
Bookstore is running on http://localhost:3000Open your browser and visit http://localhost:3000. You should see a page that says "Welcome to the Bookstore".
What does tsx watch do?
The dev script runs tsx watch server.ts. The watch flag tells tsx to monitor your files for changes. When you edit and save a file, tsx automatically restarts the server so you can see your changes immediately without manually stopping and restarting.
Your Project So Far
Here is the complete file structure at this point:
bookstore/
app/
router.ts # Creates the router, maps routes to handlers
routes.ts # Defines URL patterns
public/ # Static files (empty for now)
package.json # Project manifest and dependencies
server.ts # HTTP server entry point
tsconfig.json # TypeScript configurationAnd the flow of a request through your application:
- A browser sends an HTTP request to
http://localhost:3000/. - Node.js's
httpserver receives it and passes it tocreateRequestListener. createRequestListenerconverts it to a standardRequestobject.router.fetch(request)takes over:- The
loggermiddleware logs the request to the terminal. - The
compressionmiddleware prepares to compress the response. - The
staticFilesmiddleware checks if the URL matches a file inpublic/. It does not, so it passes the request along. - The router matches
/toroutes.homeand calls the handler.
- The
- The handler returns a
Responsewith HTML. - The
compressionmiddleware compresses the HTML. - The response flows back to the browser, which renders the page.
What is Next
Your server works, but it only has one page that returns raw HTML. In the next part, you will learn Remix's routing system in depth --- defining multiple routes, using dynamic URL segments, and organizing your routes for a full bookstore application.