Skip to content

Migrating from Express / Koa ​

A guide for moving from Express or Koa to EreoJS. This covers route mapping, middleware conversion, adding React rendering, and integrating your existing database layer.

Overview ​

Express and Koa are backend frameworks. EreoJS is a fullstack framework that combines server-side routing, data loading, and React rendering. The migration path involves:

  1. Mapping Express routes to EreoJS file-based routes
  2. Converting middleware
  3. Adding React components for rendering
  4. Keeping your existing database and business logic

Route Mapping ​

Express Routes to File Routes ​

Express routes defined in code become files in the routes/ directory:

Express:

ts
// Express
app.get('/', (req, res) => {
  res.send('Home')
})

app.get('/about', (req, res) => {
  res.send('About')
})

app.get('/posts/:id', (req, res) => {
  const post = getPost(req.params.id)
  res.json(post)
})

EreoJS:

tsx
// routes/index.tsx
export default function Home() {
  return <h1>Home</h1>
}
tsx
// routes/about.tsx
export default function About() {
  return <h1>About</h1>
}
tsx
// routes/posts/[id].tsx
import { createLoader } from '@ereo/data'

export const loader = createLoader(async ({ params }) => {
  const post = await getPost(params.id)
  return { post }
})

export default function Post({ loaderData }) {
  return <h1>{loaderData.post.title}</h1>
}

API-Only Routes ​

For routes that return JSON without rendering HTML, export HTTP method handlers:

Express:

ts
app.get('/api/users', async (req, res) => {
  const users = await db.users.findMany()
  res.json({ users })
})

app.post('/api/users', async (req, res) => {
  const user = await db.users.create(req.body)
  res.status(201).json(user)
})

EreoJS:

ts
// routes/api/users.ts
export async function GET({ request }) {
  const users = await db.users.findMany()
  return Response.json({ users })
}

export async function POST({ request }) {
  const body = await request.json()
  const user = await db.users.create(body)
  return Response.json(user, { status: 201 })
}

Key difference: EreoJS uses the Web API Request and Response objects instead of Express's req/res.

Middleware Conversion ​

Express Middleware to EreoJS Middleware ​

Express:

ts
function authMiddleware(req, res, next) {
  const token = req.headers.authorization
  if (!token) return res.status(401).json({ error: 'Unauthorized' })
  req.user = verifyToken(token)
  next()
}

app.use('/dashboard', authMiddleware)

EreoJS:

ts
// routes/dashboard/_middleware.ts
import type { MiddlewareHandler } from '@ereo/core'

export const middleware: MiddlewareHandler = async (request, context, next) => {
  const token = request.headers.get('authorization')

  if (!token) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const user = verifyToken(token)
  context.set('user', user)

  return next()
}

Key differences:

  • request is a standard Request object (use .headers.get() instead of .headers.authorization)
  • Shared data goes on context instead of mutating req
  • Return a Response instead of calling res.status().json()
  • Call next() and return its result instead of calling next() void

CORS ​

Express:

ts
app.use(cors({ origin: 'https://example.com' }))

EreoJS:

ts
// routes/_middleware.ts
export const middleware: MiddlewareHandler = async (request, context, next) => {
  const response = await next()

  response.headers.set('Access-Control-Allow-Origin', 'https://example.com')
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')

  return response
}

Adding React Rendering ​

The main upgrade from Express to EreoJS is adding server-rendered React views. Replace your template engine with React components:

Express (EJS/Pug):

ts
app.get('/posts/:id', async (req, res) => {
  const post = await getPost(req.params.id)
  res.render('post', { post })
})

EreoJS:

tsx
// routes/posts/[id].tsx
import { createLoader } from '@ereo/data'

export const loader = createLoader(async ({ params }) => {
  const post = await getPost(params.id)
  if (!post) throw new Response('Not found', { status: 404 })
  return { post }
})

export default function Post({ loaderData }) {
  return (
    <article>
      <h1>{loaderData.post.title}</h1>
      <div>{loaderData.post.content}</div>
    </article>
  )
}

The loader replaces your Express route handler's data fetching. The React component replaces your template.

Database Integration ​

Your existing database layer works as-is. EreoJS does not dictate a database or ORM:

tsx
// Use your existing database code directly in loaders
import { db } from '../lib/database' // Your existing code

export const loader = createLoader(async ({ params }) => {
  const user = await db.users.findUnique({ where: { id: params.id } })
  return { user }
})

Whether you use Prisma, Drizzle, Knex, raw SQL, or any other database library, it works the same way inside loaders and actions.

Session Handling ​

Replace Express session middleware with @ereo/plugin-auth:

Express:

ts
app.use(session({
  secret: 'my-secret',
  resave: false,
  saveUninitialized: false,
}))

EreoJS:

ts
// ereo.config.ts
import { authPlugin } from '@ereo/plugin-auth'

export default defineConfig({
  plugins: [
    authPlugin({
      session: {
        secret: process.env.SESSION_SECRET,
        maxAge: 60 * 60 * 24 * 7,
      },
    }),
  ],
})

Access the session in loaders and middleware via context.getSession(). See the Auth Plugin guide for details.

Migration Strategy ​

  1. Start a new EreoJS project alongside your Express app
  2. Copy your database layer (lib/, models/, etc.) into the EreoJS project
  3. Migrate API routes first --- these are the most direct translation
  4. Add React components for pages that were using templates
  5. Convert middleware one at a time
  6. Run both apps in parallel during the transition, proxying traffic as needed
  7. Switch over when all routes are migrated and tested

Released under the MIT License.