Tuesday, February 11, 2025

Building a Node.js GraphQL TypeScript SQLite React Vite todo app

 Let's outline a plan for building a Node.js GraphQL TypeScript SQLite Vite todo app. I'll break down the key components and considerations, providing a structure you can follow. Since a full, working application would be quite extensive, this will be a guide to get you started.

1. Project Setup:

Bash
npm init vite@latest my-todo-app --template react-ts  # Or your preferred React template
cd my-todo-app
npm install graphql @apollo/client graphql-tag typescript @types/graphql @types/node sqlite3 better-sqlite3 # For SQLite
npm install -D nodemon ts-node concurrently # For dev server

2. Backend (Node.js & GraphQL):

  • server.ts (or index.ts): This will be your main server file.
TypeScript
import express from 'express';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import cors from 'cors';
import { createSchema } from 'graphql-yoga' // or buildSchema from graphql-tools
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';

// Initialize SQLite database (using better-sqlite3 for type safety)
const dbPromise = open({
    filename: './todo.db', // Path to your database file
    driver: sqlite3.Database
}).then(async (db) => {
    await db.exec(`
        CREATE TABLE IF NOT EXISTS todos (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            text TEXT NOT NULL,
            completed BOOLEAN NOT NULL DEFAULT 0
        )
    `);
    return db;
});


// Define your GraphQL schema (using schema builder or manually)
const typeDefs = /* GraphQL */ `
  type Todo {
    id: ID!
    text: String!
    completed: Boolean!
  }

  type Query {
    todos: [Todo!]!
  }

  type Mutation {
    createTodo(text: String!): Todo!
    updateTodo(id: ID!, completed: Boolean!): Todo!
    deleteTodo(id: ID!): Boolean!
  }
`;

const resolvers = {
  Query: {
    todos: async () => {
        const db = await dbPromise;
        return await db.all('SELECT * FROM todos');
    },
  },
  Mutation: {
    createTodo: async (_, { text }) => {
        const db = await dbPromise;
        const result = await db.run('INSERT INTO todos (text) VALUES (?)', [text]);
        const id = result.lastID;
        return { id, text, completed: false };
    },
    updateTodo: async (_, { id, completed }) => {
        const db = await dbPromise;
        await db.run('UPDATE todos SET completed = ? WHERE id = ?', [completed, id]);
        return await db.get('SELECT * FROM todos WHERE id = ?', [id]);
    },
    deleteTodo: async (_, { id }) => {
        const db = await dbPromise;
        await db.run('DELETE FROM todos WHERE id = ?', [id]);
        return true;
    },
  },
};


const schema = createSchema({
    typeDefs,
    resolvers,
})



const app = express();
app.use(cors()); // Enable CORS for local development
app.use(express.json());

const server = new ApolloServer({
    schema,
});

await server.start()

app.use('/graphql', expressMiddleware(server));

const port = 4000;
app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}/graphql`);
});

  • Data Access (SQLite): The code above demonstrates basic SQLite integration using better-sqlite3 which gives type safety. The database connection is handled asynchronously using open from the sqlite package. Error handling and more robust data access logic would be added in a real application.

3. Frontend (React & Apollo Client):

  • src/App.tsx (Example):
TypeScript
import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, gql } from "@apollo/client";
import { client } from './index'; // Apollo Client instance
import { InMemoryCache } from '@apollo/client/cache';

const GET_TODOS = gql`
  query GetTodos {
    todos {
      id
      text
      completed
    }
  }
`;

const CREATE_TODO = gql`
  mutation CreateTodo($text: String!) {
    createTodo(text: $text) {
      id
      text
      completed
    }
  }
`;

const UPDATE_TODO = gql`
    mutation UpdateTodo($id: ID!, $completed: Boolean!) {
        updateTodo(id: $id, completed: $completed) {
            id
            text
            completed
        }
    }
`;

const DELETE_TODO = gql`
    mutation DeleteTodo($id: ID!) {
        deleteTodo(id: $id)
    }
`;

function App() {
  const { loading, error, data } = useQuery(GET_TODOS, { client });
  const [createTodo] = useMutation(CREATE_TODO, { client, refetchQueries: [GET_TODOS] });
  const [updateTodo] = useMutation(UPDATE_TODO, { client, refetchQueries: [GET_TODOS] });
  const [deleteTodo] = useMutation(DELETE_TODO, { client, refetchQueries: [GET_TODOS] });
  const [newTodoText, setNewTodoText] = useState("");

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error : {error.message}</p>;

  return (
    <div>
      <h1>Todo App</h1>
      <input type="text" value={newTodoText} onChange={(e) => setNewTodoText(e.target.value)} />
      <button onClick={() => {createTodo({ variables: { text: newTodoText } }); setNewTodoText("")}}>Add Todo</button>
      <ul>
        {data.todos.map((todo) => (
          <li key={todo.id}>
            <input type="checkbox" checked={todo.completed} onChange={(e) => updateTodo({ variables: { id: todo.id, completed: e.target.checked } })} />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>{todo.text}</span>
            <button onClick={() => deleteTodo({ variables: { id: todo.id } })}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

  • src/index.tsx (Apollo Client Setup):
TypeScript
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import { ApolloClient, InMemoryCache } from "@apollo/client";
import { ApolloProvider } from "@apollo/client";

export const client = new ApolloClient({
    uri: 'http://localhost:4000/graphql', // Your GraphQL server endpoint
    cache: new InMemoryCache(),
});

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <ApolloProvider client={client}>
        <App />
    </ApolloProvider>
  </React.StrictMode>
);

4. Development:

  • package.json (Scripts):
JSON
{
  "scripts": {
    "dev": "concurrently \"nodemon server.ts\" \"vite\"",
    "build": "vite build",
    "preview": "vite preview"
  }
}
  • Run npm run dev to start both the server and the Vite development server concurrently.

Key Improvements and Considerations:

  • Error Handling: Implement robust error handling in both the backend and frontend.
  • Input Validation: Sanitize and validate user input to prevent issues.
  • Pagination: For a large number of todos, implement pagination.
  • Authentication: If needed, add authentication to protect your API.
  • Testing: Write unit and integration tests for both the backend and frontend.
  • Deployment: Choose a platform to deploy your application (e.g., Heroku, Netlify, Vercel).
  • Database Migrations: Use a tool like Knex.js or TypeORM for database migrations as your schema evolves.
  • Type Safety: Leverage TypeScript's features extensively.
  • State Management (Frontend): For more complex applications, consider a state management library like Redux or Zustand.

This comprehensive guide provides a solid starting point for your Node.js GraphQL TypeScript SQLite Vite todo app. Remember to install the necessary packages and adapt the code to your

No comments:

Post a Comment