Back to all articles

Mobile App Backend & API Development: Complete Guide 2025

95% of mobile apps require backend services, yet poor backend architecture causes 70% of app performance issues. A robust backend is critical for success: fast APIs improve retention by 40%, while proper scaling prevents costly downtime. This guide covers everything from API design to production deployment.

Backend Architecture Options

BaaS (Backend as a Service)

Firebase:
✓ Quick setup (hours, not weeks)
✓ Real-time database
✓ Authentication built-in
✓ File storage included
✓ Analytics & Crashlytics
✓ Free tier generous
✗ Vendor lock-in
✗ Complex queries difficult
✗ Costs scale quickly
Best for: MVPs, small apps, rapid prototyping

Supabase:
✓ Open source (self-hostable)
✓ PostgreSQL (full SQL)
✓ Real-time subscriptions
✓ Authentication built-in
✓ Storage included
✓ Better pricing than Firebase
✗ Smaller community
✗ Fewer integrations
Best for: Apps needing SQL, avoiding lock-in

AWS Amplify:
✓ Full AWS ecosystem
✓ GraphQL APIs
✓ Offline sync
✓ Scalable infrastructure
✗ Complex setup
✗ AWS learning curve
✗ Can be expensive
Best for: AWS-experienced teams, enterprise

Parse (Open Source):
✓ Completely free
✓ Self-hosted
✓ Customizable
✗ You manage infrastructure
✗ Setup complexity
✗ Smaller ecosystem
Best for: Full control, cost-sensitive

Custom Backend

Node.js + Express:
✓ JavaScript everywhere
✓ Fast development
✓ Huge ecosystem (npm)
✓ Great for real-time
✗ Single-threaded
✗ Callback complexity
Best for: Full-stack JS teams, real-time apps

Python + Django/FastAPI:
✓ Clean, readable code
✓ Strong typing (FastAPI)
✓ Excellent libraries
✓ Great for ML integration
✗ Slower than compiled languages
Best for: Data-heavy apps, ML features

Go:
✓ Excellent performance
✓ Great concurrency
✓ Small binary size
✓ Fast compile times
✗ Smaller ecosystem
✗ Learning curve
Best for: High-performance APIs, microservices

Java + Spring Boot:
✓ Enterprise-grade
✓ Strong typing
✓ Mature ecosystem
✓ Great tooling
✗ Verbose
✗ Slower development
Best for: Enterprise, large teams

Ruby on Rails:
✓ Rapid development
✓ Convention over configuration
✓ Great for CRUD apps
✗ Slower performance
✗ Scaling challenges
Best for: Startups, rapid MVP

API Design

REST API Best Practices

URL Structure

✓ Good REST URLs:
GET    /api/v1/users              # List users
GET    /api/v1/users/123          # Get user
POST   /api/v1/users              # Create user
PUT    /api/v1/users/123          # Update user (full)
PATCH  /api/v1/users/123          # Update user (partial)
DELETE /api/v1/users/123          # Delete user

GET    /api/v1/users/123/posts    # User's posts
GET    /api/v1/posts?userId=123   # Same, query param

✗ Bad REST URLs:
GET  /api/v1/getUsers
GET  /api/v1/user/get/123
POST /api/v1/createUser
POST /api/v1/users/delete/123     # Should be DELETE

Best practices:
✓ Use nouns, not verbs
✓ Plural for collections
✓ Versioning (/v1/, /v2/)
✓ Consistent naming (camelCase or snake_case)
✓ Nested resources for relationships
✓ Query params for filtering/sorting
✓ Meaningful status codes

Request/Response Examples

// Node.js + Express example
const express = require('express');
const app = express();

app.use(express.json());

// List users with pagination
app.get('/api/v1/users', async (req, res) => {
  try {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 20;
    const offset = (page - 1) * limit;

    const users = await db.query(
      'SELECT * FROM users LIMIT $1 OFFSET $2',
      [limit, offset]
    );

    const total = await db.query('SELECT COUNT(*) FROM users');

    res.json({
      data: users.rows,
      pagination: {
        page,
        limit,
        total: parseInt(total.rows[0].count),
        totalPages: Math.ceil(total.rows[0].count / limit)
      }
    });
  } catch (error) {
    res.status(500).json({
      error: {
        message: 'Failed to fetch users',
        code: 'INTERNAL_ERROR'
      }
    });
  }
});

// Get single user
app.get('/api/v1/users/:id', async (req, res) => {
  try {
    const { id } = req.params;

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

    if (result.rows.length === 0) {
      return res.status(404).json({
        error: {
          message: 'User not found',
          code: 'USER_NOT_FOUND'
        }
      });
    }

    res.json({ data: result.rows[0] });
  } catch (error) {
    res.status(500).json({
      error: {
        message: 'Failed to fetch user',
        code: 'INTERNAL_ERROR'
      }
    });
  }
});

// Create user
app.post('/api/v1/users', async (req, res) => {
  try {
    const { email, name, password } = req.body;

    // Validation
    if (!email || !name || !password) {
      return res.status(400).json({
        error: {
          message: 'Missing required fields',
          code: 'VALIDATION_ERROR',
          fields: {
            email: !email ? 'Email is required' : null,
            name: !name ? 'Name is required' : null,
            password: !password ? 'Password is required' : null,
          }
        }
      });
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(password, 10);

    const result = await db.query(
      'INSERT INTO users (email, name, password) VALUES ($1, $2, $3) RETURNING *',
      [email, name, hashedPassword]
    );

    const user = result.rows[0];
    delete user.password; // Don't send password back

    res.status(201).json({ data: user });
  } catch (error) {
    if (error.code === '23505') { // Unique violation
      return res.status(409).json({
        error: {
          message: 'User already exists',
          code: 'USER_EXISTS'
        }
      });
    }

    res.status(500).json({
      error: {
        message: 'Failed to create user',
        code: 'INTERNAL_ERROR'
      }
    });
  }
});

// Update user
app.patch('/api/v1/users/:id', authenticateToken, async (req, res) => {
  try {
    const { id } = req.params;
    const { name, bio, avatar } = req.body;

    // Only allow user to update their own profile
    if (req.user.id !== id) {
      return res.status(403).json({
        error: {
          message: 'Forbidden',
          code: 'FORBIDDEN'
        }
      });
    }

    const updates = [];
    const values = [];
    let paramIndex = 1;

    if (name) {
      updates.push(`name = $${paramIndex++}`);
      values.push(name);
    }
    if (bio) {
      updates.push(`bio = $${paramIndex++}`);
      values.push(bio);
    }
    if (avatar) {
      updates.push(`avatar = $${paramIndex++}`);
      values.push(avatar);
    }

    values.push(id);

    const result = await db.query(
      `UPDATE users SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
      values
    );

    res.json({ data: result.rows[0] });
  } catch (error) {
    res.status(500).json({
      error: {
        message: 'Failed to update user',
        code: 'INTERNAL_ERROR'
      }
    });
  }
});

HTTP Status Codes

Use correct status codes:

Success:
200 OK              - Successful GET, PATCH, DELETE
201 Created         - Successful POST
204 No Content      - Successful DELETE (no response body)

Client Errors:
400 Bad Request     - Invalid input, validation error
401 Unauthorized    - Missing or invalid authentication
403 Forbidden       - Authenticated but not allowed
404 Not Found       - Resource doesn't exist
409 Conflict        - Duplicate resource
422 Unprocessable   - Valid syntax, semantic errors
429 Too Many Requests - Rate limit exceeded

Server Errors:
500 Internal Server Error - Generic server error
502 Bad Gateway           - Upstream server error
503 Service Unavailable   - Temporary downtime
504 Gateway Timeout       - Upstream timeout

GraphQL Alternative

# Install
npm install apollo-server-express graphql

// Type definitions
const typeDefs = gql`
  type User {
    id: ID!
    email: String!
    name: String!
    posts: [Post!]!
    createdAt: String!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    createdAt: String!
  }

  type Query {
    user(id: ID!): User
    users(limit: Int, offset: Int): [User!]!
    post(id: ID!): Post
    posts(userId: ID): [Post!]!
  }

  type Mutation {
    createUser(email: String!, name: String!, password: String!): User!
    updateUser(id: ID!, name: String, bio: String): User!
    createPost(title: String!, content: String!): Post!
    deletePost(id: ID!): Boolean!
  }
`;

// Resolvers
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      const result = await db.query('SELECT * FROM users WHERE id = $1', [id]);
      return result.rows[0];
    },
    users: async (_, { limit = 20, offset = 0 }) => {
      const result = await db.query(
        'SELECT * FROM users LIMIT $1 OFFSET $2',
        [limit, offset]
      );
      return result.rows;
    },
    post: async (_, { id }) => {
      const result = await db.query('SELECT * FROM posts WHERE id = $1', [id]);
      return result.rows[0];
    },
    posts: async (_, { userId }) => {
      if (userId) {
        const result = await db.query(
          'SELECT * FROM posts WHERE user_id = $1',
          [userId]
        );
        return result.rows;
      }
      const result = await db.query('SELECT * FROM posts');
      return result.rows;
    },
  },
  Mutation: {
    createUser: async (_, { email, name, password }) => {
      const hashedPassword = await bcrypt.hash(password, 10);
      const result = await db.query(
        'INSERT INTO users (email, name, password) VALUES ($1, $2, $3) RETURNING *',
        [email, name, hashedPassword]
      );
      return result.rows[0];
    },
    createPost: async (_, { title, content }, context) => {
      const userId = context.user.id;
      const result = await db.query(
        'INSERT INTO posts (title, content, user_id) VALUES ($1, $2, $3) RETURNING *',
        [title, content, userId]
      );
      return result.rows[0];
    },
  },
  User: {
    posts: async (user) => {
      const result = await db.query(
        'SELECT * FROM posts WHERE user_id = $1',
        [user.id]
      );
      return result.rows;
    },
  },
  Post: {
    author: async (post) => {
      const result = await db.query(
        'SELECT * FROM users WHERE id = $1',
        [post.user_id]
      );
      return result.rows[0];
    },
  },
};

// Server
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
server.applyMiddleware({ app });

// Client query (iOS/Android)
query GetUserWithPosts($userId: ID!) {
  user(id: $userId) {
    id
    name
    email
    posts {
      id
      title
      createdAt
    }
  }
}

Benefits:
✓ Request only needed fields
✓ Single endpoint
✓ Strongly typed
✓ Great tooling
✗ More complex to implement
✗ Caching more difficult

Authentication

JWT (JSON Web Tokens)

// Generate JWT
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;

function generateToken(user) {
  return jwt.sign(
    {
      id: user.id,
      email: user.email,
    },
    SECRET,
    {
      expiresIn: '7d', // Token expires in 7 days
    }
  );
}

// Login endpoint
app.post('/api/v1/auth/login', async (req, res) => {
  try {
    const { email, password } = req.body;

    // Find user
    const result = await db.query(
      'SELECT * FROM users WHERE email = $1',
      [email]
    );

    if (result.rows.length === 0) {
      return res.status(401).json({
        error: {
          message: 'Invalid credentials',
          code: 'INVALID_CREDENTIALS'
        }
      });
    }

    const user = result.rows[0];

    // Verify password
    const validPassword = await bcrypt.compare(password, user.password);
    if (!validPassword) {
      return res.status(401).json({
        error: {
          message: 'Invalid credentials',
          code: 'INVALID_CREDENTIALS'
        }
      });
    }

    // Generate token
    const token = generateToken(user);

    // Update last login
    await db.query(
      'UPDATE users SET last_login = NOW() WHERE id = $1',
      [user.id]
    );

    delete user.password;

    res.json({
      data: {
        user,
        token,
      }
    });
  } catch (error) {
    res.status(500).json({
      error: {
        message: 'Login failed',
        code: 'INTERNAL_ERROR'
      }
    });
  }
});

// Verify token middleware
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.status(401).json({
      error: {
        message: 'Authentication required',
        code: 'UNAUTHORIZED'
      }
    });
  }

  jwt.verify(token, SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({
        error: {
          message: 'Invalid or expired token',
          code: 'FORBIDDEN'
        }
      });
    }

    req.user = user;
    next();
  });
}

// Protected route
app.get('/api/v1/me', authenticateToken, async (req, res) => {
  try {
    const result = await db.query(
      'SELECT * FROM users WHERE id = $1',
      [req.user.id]
    );

    const user = result.rows[0];
    delete user.password;

    res.json({ data: user });
  } catch (error) {
    res.status(500).json({
      error: {
        message: 'Failed to fetch user',
        code: 'INTERNAL_ERROR'
      }
    });
  }
});

// iOS client
func login(email: String, password: String) async throws -> User {
    var request = URLRequest(url: URL(string: "\(baseURL)/auth/login")!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

    let body = ["email": email, "password": password]
    request.httpBody = try JSONEncoder().encode(body)

    let (data, _) = try await URLSession.shared.data(for: request)
    let response = try JSONDecoder().decode(LoginResponse.self, from: data)

    // Store token
    UserDefaults.standard.set(response.token, forKey: "authToken")

    return response.user
}

func makeAuthenticatedRequest(url: URL) async throws -> Data {
    var request = URLRequest(url: url)

    if let token = UserDefaults.standard.string(forKey: "authToken") {
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
    }

    let (data, response) = try await URLSession.shared.data(for: request)

    if let httpResponse = response as? HTTPURLResponse {
        if httpResponse.statusCode == 401 {
            // Token expired, redirect to login
            throw AuthError.unauthorized
        }
    }

    return data
}

OAuth 2.0 (Social Login)

# Install
npm install passport passport-google-oauth20 passport-facebook

// Google OAuth setup
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: '/api/v1/auth/google/callback'
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      // Find or create user
      let result = await db.query(
        'SELECT * FROM users WHERE google_id = $1',
        [profile.id]
      );

      let user;
      if (result.rows.length === 0) {
        // Create new user
        result = await db.query(
          'INSERT INTO users (google_id, email, name, avatar) VALUES ($1, $2, $3, $4) RETURNING *',
          [profile.id, profile.emails[0].value, profile.displayName, profile.photos[0].value]
        );
        user = result.rows[0];
      } else {
        user = result.rows[0];
      }

      done(null, user);
    } catch (error) {
      done(error, null);
    }
  }
));

app.get('/api/v1/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/api/v1/auth/google/callback',
  passport.authenticate('google', { session: false }),
  (req, res) => {
    const token = generateToken(req.user);
    // Redirect to app with token
    res.redirect(`myapp://auth?token=${token}`);
  }
);

Database Design

Schema Example

-- PostgreSQL schema
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) UNIQUE NOT NULL,
  password VARCHAR(255),
  name VARCHAR(255) NOT NULL,
  bio TEXT,
  avatar VARCHAR(500),
  google_id VARCHAR(255) UNIQUE,
  apple_id VARCHAR(255) UNIQUE,
  email_verified BOOLEAN DEFAULT FALSE,
  is_premium BOOLEAN DEFAULT FALSE,
  last_login TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_google_id ON users(google_id);

CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  title VARCHAR(500) NOT NULL,
  content TEXT NOT NULL,
  image_url VARCHAR(500),
  likes_count INTEGER DEFAULT 0,
  comments_count INTEGER DEFAULT 0,
  is_published BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);

CREATE TABLE comments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  content TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_comments_post_id ON comments(post_id);
CREATE INDEX idx_comments_user_id ON comments(user_id);

CREATE TABLE follows (
  follower_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  following_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  created_at TIMESTAMP DEFAULT NOW(),
  PRIMARY KEY (follower_id, following_id)
);

CREATE INDEX idx_follows_follower ON follows(follower_id);
CREATE INDEX idx_follows_following ON follows(following_id);

-- Triggers for updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_posts_updated_at BEFORE UPDATE ON posts
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

File Upload

S3 Upload

# Install
npm install aws-sdk multer multer-s3

// Configure S3
const AWS = require('aws-sdk');
const multer = require('multer');
const multerS3 = require('multer-s3');

const s3 = new AWS.S3({
  accessKeyId: process.env.AWS_ACCESS_KEY,
  secretAccessKey: process.env.AWS_SECRET_KEY,
  region: process.env.AWS_REGION
});

const upload = multer({
  storage: multerS3({
    s3: s3,
    bucket: process.env.S3_BUCKET,
    acl: 'public-read',
    metadata: (req, file, cb) => {
      cb(null, { fieldName: file.fieldname });
    },
    key: (req, file, cb) => {
      const fileName = `${Date.now()}-${file.originalname}`;
      cb(null, fileName);
    }
  }),
  limits: {
    fileSize: 5 * 1024 * 1024 // 5MB
  },
  fileFilter: (req, file, cb) => {
    if (file.mimetype.startsWith('image/')) {
      cb(null, true);
    } else {
      cb(new Error('Only images allowed'), false);
    }
  }
});

// Upload endpoint
app.post('/api/v1/upload',
  authenticateToken,
  upload.single('file'),
  (req, res) => {
    if (!req.file) {
      return res.status(400).json({
        error: {
          message: 'No file uploaded',
          code: 'NO_FILE'
        }
      });
    }

    res.json({
      data: {
        url: req.file.location,
        key: req.file.key
      }
    });
  }
);

Real-time Features

WebSockets

# Install
npm install socket.io

// Server
const server = require('http').createServer(app);
const io = require('socket.io')(server);

io.use((socket, next) => {
  const token = socket.handshake.auth.token;

  jwt.verify(token, SECRET, (err, user) => {
    if (err) {
      return next(new Error('Authentication error'));
    }
    socket.user = user;
    next();
  });
});

io.on('connection', (socket) => {
  console.log('User connected:', socket.user.id);

  socket.on('join-room', (roomId) => {
    socket.join(roomId);
    io.to(roomId).emit('user-joined', {
      userId: socket.user.id,
      username: socket.user.name
    });
  });

  socket.on('send-message', async (data) => {
    const { roomId, message } = data;

    // Save to database
    const result = await db.query(
      'INSERT INTO messages (room_id, user_id, content) VALUES ($1, $2, $3) RETURNING *',
      [roomId, socket.user.id, message]
    );

    // Broadcast to room
    io.to(roomId).emit('new-message', {
      id: result.rows[0].id,
      userId: socket.user.id,
      username: socket.user.name,
      message: message,
      timestamp: new Date()
    });
  });

  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.user.id);
  });
});

// iOS client with Socket.IO
import SocketIO

let manager = SocketIOManager(
  socketURL: URL(string: "https://api.example.com")!,
  config: [
    .log(true),
    .compress,
    .extraHeaders(["Authorization": "Bearer \(token)"])
  ]
)

let socket = manager.defaultSocket

socket.on(clientEvent: .connect) { data, ack in
  print("Connected")
  socket.emit("join-room", ["roomId": "123"])
}

socket.on("new-message") { data, ack in
  guard let messageData = data[0] as? [String: Any] else { return }
  // Handle new message
}

func sendMessage(_ message: String, roomId: String) {
  socket.emit("send-message", [
    "roomId": roomId,
    "message": message
  ])
}

socket.connect()

Caching

Redis

# Install
npm install redis

// Setup
const redis = require('redis');
const client = redis.createClient({
  url: process.env.REDIS_URL
});

await client.connect();

// Cache middleware
async function cacheMiddleware(req, res, next) {
  const key = req.originalUrl;

  try {
    const cachedData = await client.get(key);

    if (cachedData) {
      return res.json(JSON.parse(cachedData));
    }

    // Store original send
    const originalSend = res.json.bind(res);

    // Override send
    res.json = (data) => {
      // Cache for 5 minutes
      client.setEx(key, 300, JSON.stringify(data));
      originalSend(data);
    };

    next();
  } catch (error) {
    next();
  }
}

// Use cache
app.get('/api/v1/posts', cacheMiddleware, async (req, res) => {
  // This will be cached
  const posts = await db.query('SELECT * FROM posts');
  res.json({ data: posts.rows });
});

// Invalidate cache
async function invalidatePostsCache() {
  await client.del('/api/v1/posts');
}

Deployment

Docker

# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/myapp
      REDIS_URL: redis://redis:6379
      JWT_SECRET: your-secret
    depends_on:
      - db
      - redis

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  postgres_data:

# Build and run
docker-compose up -d

Conclusion

Building a robust backend is critical for mobile app success. Whether you choose a BaaS for speed or custom solution for flexibility, focus on security, performance, and scalability from day one. Proper API design, authentication, database schema, and deployment infrastructure set the foundation for apps that can grow from thousands to millions of users.

Professional backends need professional frontends. Our support URL generator creates fast, secure support pages that integrate seamlessly with your backend infrastructure and provide excellent user experience.

Need a Support URL for Your App?

Generate a compliant, professional support page in under a minute. Our easy-to-use generator creates everything you need for App Store and Google Play submissions.