Node.js Backend Development for Mobile Applications
A mobile app without a backend is just offline notes. Once users appear, data sync, push notifications, payments — you need a server. Node.js + TypeScript is a pragmatic choice for most mobile backends: fast startup, JSON-native, enormous ecosystem, one language for the team if frontend is also TypeScript.
Stack and Components
Fastify or Express. Express is familiar and documented everywhere. Fastify is faster (20–30% higher throughput in tests), schema-first validation via JSON Schema, out-of-the-box TypeScript support. For new projects, choose Fastify.
// Fastify + TypeScript + Zod validation
import Fastify from 'fastify';
import { z } from 'zod';
const app = Fastify({ logger: true });
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
authorId: z.string().uuid(),
});
app.post('/posts', async (request, reply) => {
const body = CreatePostSchema.parse(request.body);
const post = await postService.create(body);
return reply.status(201).send(post);
});
PostgreSQL + Prisma ORM. Prisma provides type-safe queries from TypeScript schema:
model User {
id String @id @default(uuid())
email String @unique
posts Post[]
createdAt DateTime @default(now())
}
const user = await prisma.user.findUnique({
where: { id: userId },
include: { posts: { take: 10, orderBy: { createdAt: 'desc' } } },
});
Prisma Studio — built-in GUI for data inspection during development. Prisma Migrate — database schema versioning via migration files.
Mobile Client Authentication
For mobile apps — JWT with short access token (15 min) and long refresh token (30 days) in secure storage on device. Rotate refresh token on each use.
import jwt from 'jsonwebtoken';
export const generateTokens = (userId: string) => ({
accessToken: jwt.sign({ sub: userId, type: 'access' }, process.env.JWT_SECRET!, {
expiresIn: '15m',
}),
refreshToken: jwt.sign({ sub: userId, type: 'refresh' }, process.env.JWT_REFRESH_SECRET!, {
expiresIn: '30d',
}),
});
// Verification middleware
export const authMiddleware = async (request: FastifyRequest, reply: FastifyReply) => {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) return reply.status(401).send({ error: 'Unauthorized' });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
request.userId = payload.sub!;
} catch {
return reply.status(401).send({ error: 'Invalid token' });
}
};
Store refresh token in database (refresh_tokens table) — allows invalidating all user sessions on password change or logout from all devices.
Push Notifications via Firebase Admin SDK
import * as admin from 'firebase-admin';
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) });
export const sendPushNotification = async (
fcmToken: string,
title: string,
body: string,
data?: Record<string, string>
) => {
const message: admin.messaging.Message = {
token: fcmToken,
notification: { title, body },
data,
apns: {
payload: { aps: { sound: 'default', badge: 1 } },
},
android: {
priority: 'high',
notification: { sound: 'default' },
},
};
return admin.messaging().send(message);
};
For bulk sends — sendEachForMulticast with batches of 500 tokens. Remove invalid tokens (messaging/registration-token-not-registered) from database.
Realtime: WebSocket or Server-Sent Events
Chat, realtime likes, delivery status — require persistent connections. WebSocket via ws or socket.io:
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ server: httpServer });
const connections = new Map<string, WebSocket>();
wss.on('connection', (ws, request) => {
const userId = getUserIdFromRequest(request);
connections.set(userId, ws);
ws.on('close', () => connections.delete(userId));
});
export const notifyUser = (userId: string, event: object) => {
const ws = connections.get(userId);
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(event));
}
};
For horizontal scaling (multiple instances), WebSocket connection binds to specific server — need Redis Pub/Sub for broadcasting between instances.
Infrastructure
Docker + docker-compose for local development. PM2 or Docker for production. PostgreSQL main database, Redis for cache and queues (BullMQ). S3 for files.
API structure:
src/
modules/
users/ # controller, service, repository, dto
posts/
notifications/
shared/
middleware/
guards/
utils/
app.ts
server.ts
Layering: controller → service → repository. Service doesn't know HTTP context — tests purely via Jest.
Common Mistakes
Blocking operations in event loop. JSON.parse large file, synchronous fs.readFileSync per request — blocks all others. Heavy operations go to worker threads or offload to queue.
N+1 in Prisma. Query post list without include → separate query for each post's author. Solution: always use include or Prisma $queryRaw with JOIN for complex cases.
No rate limiting. Mobile client sends 100 requests per second on bad network code — server crashes. Fastify Rate Limit (@fastify/rate-limit) + Redis for distributed limiting.
What's Included in Development
API contract design (OpenAPI/Swagger). Infrastructure setup (database, Redis, Docker). Auth implementation (JWT + refresh). CRUD modules per app requirements. Push notifications. CI/CD configuration. API documentation.
Timeline
MVP backend (Auth + 3–5 resources + push): 2–3 weeks. Full backend with realtime, payments, files: 1–3 months. Pricing calculated after functional requirements review.







