home / skills / encoredev / skills / migrate

migrate skill

/encore/migrate

This skill helps migrate Express or Fastify apps to Encore.ts by converting routes, requests, and middleware into Encore API patterns.

npx playbooks add skill encoredev/skills --skill migrate

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.8 KB
---
name: encore-migrate
description: Migrate Express/Fastify apps to Encore.ts.
---

# Migrate to Encore.ts

## Instructions

When migrating existing Node.js applications to Encore.ts, follow these transformation patterns:

## Express to Encore

### Basic Route

```typescript
// BEFORE: Express
const express = require('express');
const app = express();

app.get('/users/:id', async (req, res) => {
  const user = await getUser(req.params.id);
  res.json(user);
});

app.listen(3000);
```

```typescript
// AFTER: Encore
import { api } from "encore.dev/api";

interface GetUserRequest {
  id: string;
}

interface User {
  id: string;
  email: string;
  name: string;
}

export const getUser = api(
  { method: "GET", path: "/users/:id", expose: true },
  async ({ id }: GetUserRequest): Promise<User> => {
    return await findUser(id);
  }
);
```

### POST with Body

```typescript
// BEFORE: Express
app.post('/users', async (req, res) => {
  const { email, name } = req.body;
  const user = await createUser(email, name);
  res.status(201).json(user);
});
```

```typescript
// AFTER: Encore
interface CreateUserRequest {
  email: string;
  name: string;
}

export const createUser = api(
  { method: "POST", path: "/users", expose: true },
  async (req: CreateUserRequest): Promise<User> => {
    return await insertUser(req.email, req.name);
  }
);
```

### Query Parameters

```typescript
// BEFORE: Express
app.get('/users', async (req, res) => {
  const { limit, offset } = req.query;
  const users = await listUsers(Number(limit), Number(offset));
  res.json(users);
});
```

```typescript
// AFTER: Encore
import { Query, api } from "encore.dev/api";

interface ListUsersRequest {
  limit?: Query<number>;
  offset?: Query<number>;
}

export const listUsers = api(
  { method: "GET", path: "/users", expose: true },
  async ({ limit = 10, offset = 0 }: ListUsersRequest): Promise<{ users: User[] }> => {
    return { users: await fetchUsers(limit, offset) };
  }
);
```

### Headers

```typescript
// BEFORE: Express
app.post('/webhook', async (req, res) => {
  const signature = req.headers['x-signature'];
  // verify...
});
```

```typescript
// AFTER: Encore
import { Header, api } from "encore.dev/api";

interface WebhookRequest {
  signature: Header<"X-Signature">;
  payload: any;
}

export const webhook = api(
  { method: "POST", path: "/webhook", expose: true },
  async ({ signature, payload }: WebhookRequest): Promise<void> => {
    // verify signature...
  }
);
```

### Raw Request Access (Webhooks)

```typescript
// BEFORE: Express
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  const event = stripe.webhooks.constructEvent(req.body, sig, secret);
  res.sendStatus(200);
});
```

```typescript
// AFTER: Encore
export const stripeWebhook = api.raw(
  { expose: true, path: "/webhooks/stripe", method: "POST" },
  async (req, res) => {
    const sig = req.headers["stripe-signature"];
    const chunks: Buffer[] = [];
    for await (const chunk of req) {
      chunks.push(chunk);
    }
    const body = Buffer.concat(chunks);
    const event = stripe.webhooks.constructEvent(body, sig, secret);
    res.writeHead(200);
    res.end();
  }
);
```

### Middleware

```typescript
// BEFORE: Express
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next();
});
```

```typescript
// AFTER: Encore
import { Service } from "encore.dev/service";
import { middleware } from "encore.dev/api";

const logMiddleware = middleware(
  { target: { all: true } },
  async (req, next) => {
    console.log(`${req.requestMeta?.method} ${req.requestMeta?.path}`);
    return next(req);
  }
);

export default new Service("my-service", {
  middlewares: [logMiddleware],
});
```

### Error Handling

```typescript
// BEFORE: Express
app.get('/users/:id', async (req, res) => {
  const user = await getUser(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});
```

```typescript
// AFTER: Encore
import { APIError, api } from "encore.dev/api";

export const getUser = api(
  { method: "GET", path: "/users/:id", expose: true },
  async ({ id }: GetUserRequest): Promise<User> => {
    const user = await findUser(id);
    if (!user) {
      throw APIError.notFound("user not found");
    }
    return user;
  }
);
```

## Database Migration

```typescript
// BEFORE: Express with pg
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

const result = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
```

```typescript
// AFTER: Encore
import { SQLDatabase } from "encore.dev/storage/sqldb";

const db = new SQLDatabase("users", {
  migrations: "./migrations",
});

const user = await db.queryRow<User>`
  SELECT * FROM users WHERE id = ${id}
`;
```

## Cron Jobs

```typescript
// BEFORE: Node with node-cron
import cron from 'node-cron';

cron.schedule('0 * * * *', () => {
  cleanupExpiredSessions();
});
```

```typescript
// AFTER: Encore
import { CronJob } from "encore.dev/cron";
import { api } from "encore.dev/api";

export const cleanupSessions = api(
  { expose: false },
  async (): Promise<void> => {
    // cleanup logic
  }
);

const _ = new CronJob("cleanup-sessions", {
  title: "Cleanup expired sessions",
  schedule: "0 * * * *",
  endpoint: cleanupSessions,
});
```

## Migration Checklist

- [ ] Replace `require` with `import`
- [ ] Remove `app.listen()` - Encore handles this
- [ ] Convert routes to `api()` functions
- [ ] Define TypeScript interfaces for request/response
- [ ] Replace manual validation with Encore's type validation
- [ ] Convert error responses to `APIError`
- [ ] Move database connection to `SQLDatabase`
- [ ] Convert cron jobs to `CronJob`
- [ ] Move env vars to `secret()` for sensitive values
- [ ] Create `encore.service.ts` in each service directory

Overview

This skill helps developers migrate existing Express or Fastify Node.js apps to Encore.ts by providing concrete transformation patterns, code examples, and a migration checklist. It focuses on routes, middleware, webhooks, raw request handling, database access, cron jobs, and error handling to speed up conversion and enforce Encore idioms. The guidance is practical and TypeScript-first to minimize runtime surprises.

How this skill works

The skill inspects common Express/Fastify patterns and maps them to Encore equivalents: api() endpoints, middleware, CronJob, SQLDatabase, and APIError handling. It provides typed request/response interfaces, query/header wrappers, and raw request handlers for webhooks. It also recommends structural changes like replacing require with import and centralizing secrets and migrations.

When to use it

  • You are migrating an Express or Fastify codebase to Encore.ts.
  • You want to adopt Encore's typed api() endpoints and built-in request validation.
  • You need to convert raw webhook handlers (e.g., Stripe) to Encore raw handlers.
  • You are replacing direct pg/sql usage with Encore SQLDatabase and migrations.
  • You want to standardize cron jobs, middleware, and error handling across services.

Best practices

  • Define TypeScript interfaces for all request and response shapes to leverage Encore validation.
  • Replace app.listen() and let Encore manage service lifecycle.
  • Use APIError for non-2xx responses instead of manual res.status logic.
  • Use SQLDatabase with tagged template queries and a migrations folder for schema management.
  • Move sensitive values to secret() and avoid in-code environment string literals.
  • Create an encore.service.ts per service and register middlewares and cron jobs there.

Example use cases

  • Convert GET/POST Express routes to exported api() functions with typed payloads.
  • Migrate Stripe or other webhook endpoints to api.raw handlers that buffer the raw body and validate signatures.
  • Replace middleware logging and auth from app.use to Encore middleware with target filters.
  • Swap direct pg Pool queries for SQLDatabase.queryRow and managed migrations.
  • Transform node-cron scheduled tasks into CronJob-driven endpoints invoked by Encore scheduler.

FAQ

Do I need to rewrite all routes at once?

No. Convert routes incrementally: export new api() endpoints alongside existing routes and cut over traffic progressively.

How do I handle third-party middleware (e.g., body-parser)?

Most Express middleware is unnecessary; use Encore request typing and api.raw for raw bodies. For complex middleware, reimplement as Encore middleware or inline logic in endpoints.