Encore.ts — Infrastructure From Code, or How to Never Write Terraform Again
Advertisement
Introduction
Infrastructure-as-code tools like Terraform require learning HCL and managing state files. Encore.ts takes the opposite approach: declare infrastructure directly in your application code. The framework infers databases, APIs, services, and deployment requirements from TypeScript decorators. This guide covers building and deploying with Encore.
What Encore.TS Does
Encore automatically manages:
- API endpoints via
@apidecorator - SQL databases via
@encore.dbwith auto-migrations - Message queues (pubsub) via
@encore.pubsub - Scheduled tasks via
@encore.cron - Secrets via
Encore.meta.environment - Service-to-service calls with automatic type safety
Your code is the source of truth. Encore generates CloudFormation, Terraform, or Kubernetes manifests.
Declaring HTTP Endpoints
The @api decorator exposes TypeScript functions as REST endpoints:
import { api } from 'encore.dev/api'
interface User {
id: number
name: string
email: string
}
export const getUser = api(
{ method: 'GET', path: '/users/:id', expose: true },
async (req: { id: number }): Promise<User> => {
return {
id: req.id,
name: 'Alice',
email: 'alice@example.com',
}
}
)
export const createUser = api(
{ method: 'POST', path: '/users', expose: true },
async (req: { name: string; email: string }): Promise<User> => {
return {
id: 1,
...req,
}
}
)Encore infers the OpenAPI schema from parameter and return types. You get auto-generated docs, client SDKs, and type safety.
TypeScript Auto-Migrations with @encore.db
Declare databases without SQL:
import { db } from 'encore.dev/db'
interface User {
id: number
name: string
email: string
createdAt: Date
}
export const usersDB = db.createClient('users', {
migration: `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
`,
})
export const createUser = api(
{ method: 'POST', path: '/users' },
async (req: { name: string; email: string }): Promise<User> => {
const result = await usersDB.query(
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
[req.name, req.email]
)
return result.rows[0]
}
)
export const getUser = api(
{ method: 'GET', path: '/users/:id' },
async (req: { id: number }): Promise<User> => {
const result = await usersDB.query(
'SELECT * FROM users WHERE id = $1',
[req.id]
)
return result.rows[0]
}
)Encore runs migrations on encore run and encore deploy. No separate migration tools needed.
PubSub for Message Queues
Declare message queues as first-class primitives:
import { pubsub } from 'encore.dev/pubsub'
interface UserCreatedEvent {
userId: number
email: string
}
export const userCreated = pubsub.newTopic('user-created', {
deliveryGuarantee: 'at-least-once',
})
export const createUser = api(
{ method: 'POST', path: '/users' },
async (req: { name: string; email: string }) => {
// Create user...
const userId = 1
const email = req.email
// Publish event
await userCreated.publish({
userId,
email,
})
return { userId, email }
}
)
// Subscriber
export const onUserCreated = userCreated.subscribe(
'send-welcome-email',
async (msg: UserCreatedEvent) => {
console.log(`Sending welcome email to ${msg.email}`)
// Send email via Mailgun, SendGrid, etc.
}
)Subscribers are automatically scaled. Encore handles delivery and retries.
Cron Jobs
Schedule tasks with simple decorators:
import { cron } from 'encore.dev/cron'
export const dailyDigest = cron.newJob('daily-digest', {
title: 'Send daily digest',
every: '24h',
retryPolicy: {
maxAttempts: 3,
backoffFactor: 2,
},
})
export const handler = dailyDigest(async () => {
console.log('Sending daily digest...')
// Fetch users, aggregate data, send emails
})Cron schedules are declared in code. No separate cron orchestration needed.
Secrets Management
Store secrets without .env files:
import { meta } from 'encore.dev'
export const sendEmail = api(
{ method: 'POST', path: '/send-email' },
async (req: { to: string; subject: string; body: string }) => {
const apiKey = meta.environment.MAILGUN_API_KEY
// Use apiKey to call Mailgun...
return { success: true }
}
)Secrets are stored in Encore''s vault. Access them via meta.environment. No env files in version control.
Service-to-Service Calls with Type Safety
Call other Encore services with full type safety:
// users/users.ts
export const getUser = api(
{ method: 'GET', path: '/users/:id' },
async (req: { id: number }): Promise<User> => {
// ...
}
)
// posts/posts.ts
import * as users from '../users'
export const getUserPosts = api(
{ method: 'GET', path: '/posts/user/:userId' },
async (req: { userId: number }) => {
// Call users service with full type inference
const user = await users.getUser({ id: req.userId })
const posts = await db.query(
'SELECT * FROM posts WHERE user_id = $1',
[user.id]
)
return posts
}
)Encore generates service definitions automatically. No HTTP URLs, no manual type definitions.
Local Development with encore run
Test locally before deploying:
encore run
# Output:
# Listening on http://localhost:4000
# Dashboard at http://localhost:4000/encore/dashboardThe dashboard shows:
- Request/response tracing
- Database queries
- Service dependencies
- Performance metrics
Test with curl:
curl -X POST http://localhost:4000/users \
-H 'Content-Type: application/json' \
-d '{"name":"Alice","email":"alice@example.com"}'Deploying to AWS, GCP, Azure
Encore generates infrastructure:
# Connect to cloud provider
encore auth login
# Deploy to production
encore deploy production
# Output:
# Deploying to AWS...
# ✓ API Gateway configured
# ✓ RDS PostgreSQL provisioned
# ✓ Lambda functions deployed
# ✓ SQS queues created
# ✓ CloudWatch alarms configuredEncore provisions:
- AWS: API Gateway, Lambda, RDS, SQS, CloudWatch
- GCP: Cloud Run, Cloud SQL, Pub/Sub, Cloud Trace
- Azure: App Service, Azure SQL, Service Bus
No Terraform. No CloudFormation. Encore handles it.
Tracing and Metrics
Built-in observability:
import { telemetry } from 'encore.dev/telemetry'
export const createUser = api(
{ method: 'POST', path: '/users' },
async (req: { name: string; email: string }) => {
telemetry.recordMetric('users.created', 1)
const user = {
id: Math.random(),
...req,
}
telemetry.recordSpan('user.validation', () => {
// Validation logic
})
return user
}
)Traces are exported to Datadog, Honeycomb, or Otel collectors automatically.
Environment Configuration
Manage different environments (dev, staging, prod):
// encore.app.ts
export default new AppConfig({
name: 'my-api',
environments: {
dev: {
location: 'us-west-1',
replicas: 1,
},
staging: {
location: 'us-west-1',
replicas: 2,
},
prod: {
location: 'us-west-1',
replicas: 3,
autoScaling: {
minReplicas: 3,
maxReplicas: 10,
targetCPUUtilization: 70,
},
},
},
})encore deploy uses the environment-specific config.
When Encore Makes Sense
Use Encore if:
- You''re building REST APIs
- Your infrastructure is standard (PostgreSQL, APIs, queues)
- You want to avoid Terraform/CloudFormation
- Your team is TypeScript-focused
Avoid Encore if:
- You have exotic infrastructure (Cassandra, Kubernetes)
- You need fine-grained infrastructure control
- You have existing Terraform code
- Your organization requires multi-cloud flexibility
Encore is opinionated. It trades flexibility for simplicity.
Encore vs Serverless Framework
| Aspect | Encore | Serverless Framework |
|---|---|---|
| Language | TypeScript only | Any language (via plugins) |
| Infrastructure | Declarative | Mostly declarative |
| Migration | Easy from Express | Difficult from Express |
| Cloud providers | 3 (AWS, GCP, Azure) | 6+ (AWS, GCP, Azure, Heroku, etc.) |
| DX | Excellent | Good |
| Maturity | Growing | Mature |
Encore is newer but has superior developer experience. Serverless Framework is more flexible.
Checklist
- Install Encore CLI
- Create project with
encore create - Define API endpoints with
@api - Set up PostgreSQL database with
@encore.db - Add PubSub topics and subscribers
- Schedule cron jobs
- Test locally with
encore run - Set up secrets in Encore''s vault
- Configure environments
- Deploy to production with
encore deploy - Monitor tracing and metrics
Conclusion
Encore.ts bridges the gap between application code and infrastructure. By declaring infrastructure in TypeScript, you eliminate context-switching to YAML or HCL. Migrations, scaling, and deployments are one command. It won''t replace Terraform for complex multi-cloud setups, but for typical REST APIs, Encore is faster, simpler, and more maintainable. For new projects, it should be your default.
Advertisement